@gluecharm-lab/easyspecs-cli 0.0.19 → 0.0.20

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/README.md CHANGED
@@ -5,7 +5,7 @@ Headless **EasySpecs** command-line tool: same orchestration ideas as the **Easy
5
5
  ## The three factory-level commands
6
6
 
7
7
  1. **`easyspecs-cli analysis` — create new context**
8
- Runs the full **Generate Context** factory end to end (synthesis until stable, coverage, remediation, reporting, link mapping, index assembly). Use this when you need a fresh or full rebuild of **`.gluecharm/context`** from the repo. **`--force-new-context-analysis`** overrides skip-when-already-analyzed behavior when **`easyspecs.factory.cloudContextAnalyzed`** would otherwise short-circuit the run. After **`analysis`**, run **`easyspecs-cli upload context`** or **`upload republish`** when you want context on EasySpecs (**`auth login`** + **`config set-project-id`**). If **`git worktree add`** fails because a temp analysis checkout folder was removed but Git still lists it, the CLI runs **`git worktree prune`** in your repository and retries **`git worktree add`** once (same behaviour wherever the CLI creates that temp worktree).
8
+ Runs the full **Generate Context** factory end to end (synthesis until stable, coverage, remediation, reporting, link mapping, index assembly). Use this when you need a fresh or full rebuild of **`.gluecharm/context`** from the repo. **`--force-new-context-analysis`** overrides skip-when-already-analyzed behavior when **`easyspecs.factory.cloudContextAnalyzed`** would otherwise short-circuit the run. On a **successful** factory exit, the CLI **always** copies **`.gluecharm/context`** from the analysis worktree into **`<repoRoot>`** unless you pass **`--no-promote`** (no **`--promote`** needed); stderr **`[pipeline:analysis] promoted …`**. This step does **not** use **`easyspecs.analysis.promoteContextToWorkspace`** — that setting still applies to **`run synthesis`** and **`update context`**. After **`analysis`**, run **`easyspecs-cli upload context`** or **`upload republish`** when you want context on EasySpecs (**`auth login`** + **`config set-project-id`**). If **`git worktree add`** fails because a temp analysis checkout folder was removed but Git still lists it, the CLI runs **`git worktree prune`** in your repository and retries **`git worktree add`** once (same behaviour wherever the CLI creates that temp worktree).
9
9
 
10
10
  2. **`easyspecs-cli update context` — update context from code churn**
11
11
  Incremental refresh from a **git delta** since the stored baseline (**`easyspecs.factory.updateContext.lastRunAt`**) or, when that is absent, inferred from workspace context mtimes: materialises **`changes-since-date.md`**, runs **scoped** remediation when commits and touched paths warrant it, then optional promotion. Use **`upload context`** afterward when you need cloud sync. Use this day to day after **`analysis`** when the codebase moved but specs did not restart from zero.
@@ -224,8 +224,8 @@ These appear **before** the subcommand (everything after the first non-flag toke
224
224
  | `--api-base-url <url>` | System Manager API origin for this process (overrides `easyspecs.apiBaseUrl`). |
225
225
  | `--session-path <file>` | Session JSON path for **this process only** (overrides **`easyspecs.cliSessionPath`**); does not rewrite **`config.json`** unless you use **`auth login`** tail **`--session-path`** as documented under **Auth**. |
226
226
  | `--environment production` \| `staging` | Alias **`--env`**. Overrides `easyspecs.deploymentEnvironment` for built-in URL selection when no explicit URL is set. |
227
- | `--promote` | After **`run synthesis`**, copy generated **`.gluecharm/context`** from the analysis worktree into the workspace repo when applicable. |
228
- | `--no-promote` | Disable that promotion for this run. |
227
+ | `--promote` | After **`run synthesis`**: force copying **`.gluecharm/context`** into the repo when **`easyspecs.analysis.promoteContextToWorkspace`** would disable it. **`analysis`** does not need this flag — promotion is automatic unless **`--no-promote`**. |
228
+ | `--no-promote` | After **`run synthesis`** or **`analysis`**: skip copying **`.gluecharm/context`** into the repo for this run. |
229
229
  | `--help`, `-h` | Built-in help. Does **not** create **`.easyspecs/config.json`**. |
230
230
  | `--version`, `-V` | CLI package version. Does **not** create config. |
231
231
  | `--config <path>` | Parsed but **unused** in current releases; read **`<repo>/.easyspecs/config.json`** instead. |
@@ -258,7 +258,7 @@ Every command the CLI accepts, with command-specific tokens. Global flags above
258
258
  | `run synthesis` | — | Context artefact pipeline pass; **`requireOpenCode`** applies. **`--promote` / `--no-promote`** affect promotion after success. |
259
259
  | `run synthesis resume-missing` | **`--worktree <path>`** (optional) | Parallel missing-artefact remediation pool on an existing checkout (from **`--worktree`** or last snapshot). **`requireOpenCode`**. |
260
260
  | `run synthesis resume-synthesis` | **`--worktree <path>`** (optional) | Same implementation as **`resume-missing`** (alias command path). |
261
- | `analysis` | **`--force-new-context-analysis`** (optional; tail only). **`--upload`** / **`--skip-upload`** → usage exit; **`--synthesis-only`** → ignored + stderr deprecation (**SRS-60**). | Full **Generate Context** factory — **create new context** trajectory. CLI **does not** run backend upload inside **`analysis`**; use **`upload context`** / **`upload republish`** afterward. If **`easyspecs.factory.cloudContextAnalyzed`** is **`true`** and **`--force-new-context-analysis`** is absent, exits **0** early with **`analysisSkipped`**. Stale temp-worktree registration: **`git worktree prune`** then one retry of **`git worktree add`**. |
261
+ | `analysis` | **`--force-new-context-analysis`** (optional; tail only). **`--upload`** / **`--skip-upload`** → usage exit; **`--synthesis-only`** → ignored + stderr deprecation (**SRS-60**). | Full **Generate Context** factory — **create new context** trajectory. CLI **does not** run backend upload inside **`analysis`**; use **`upload context`** / **`upload republish`** afterward. **Promotion** after success is **automatic** unless **`--no-promote`** (independent of **`easyspecs.analysis.promoteContextToWorkspace`**). If **`easyspecs.factory.cloudContextAnalyzed`** is **`true`** and **`--force-new-context-analysis`** is absent, exits **0** early with **`analysisSkipped`**. Stale temp-worktree registration: **`git worktree prune`** then one retry of **`git worktree add`**. |
262
262
  | `update context` | — | **Update context** trajectory: git window after baseline → worktree → **`changes-since-date.md`** → optional remediation → **`--promote` / `--no-promote`** → **`easyspecs.factory.updateContext.lastRunAt`**. Run **`upload context`** separately for cloud sync. Seed context should exist on **HEAD** in the checkout (the factory may copy from workspace when the worktree lacks context). |
263
263
  | `diagnose reference-coverage` | **`--root workspace`** \| **`--root worktree`**, optional **`--worktree <path>`** | Reference coverage gate; optional percent limit via **`easyspecs.diagnose.zeroReference.maxPercentNonReferenced`**. |
264
264
  | `diagnose coordination-duplicates` | same | Duplicate/orphan reporting; **`easyspecs.diagnose.coordinationDuplicates.strict`**. |
package/commands.md CHANGED
@@ -57,8 +57,8 @@ These flags may appear **before** the subcommand. Parsing and command registrati
57
57
  | `--verbose` | Extra stderr logging where implemented. |
58
58
  | `--api-base-url <url>` | System Manager API origin for this process (overrides `easyspecs.apiBaseUrl` in config). |
59
59
  | `--environment production` | `--environment staging` | Alias: `**--env**`. Overrides `easyspecs.deploymentEnvironment` for built-in URL selection when no explicit URL is set. |
60
- | `--promote` | After a successful `run synthesis`, copy generated `.gluecharm/context` from the analysis worktree into the workspace repo (when promotion is applicable). |
61
- | `--no-promote` | Disable that promotion step for this run. |
60
+ | `--promote` | After **`run synthesis`**: force promotion into the repo when **`easyspecs.analysis.promoteContextToWorkspace`** would turn it off. **`analysis`**: not required for promotion (promotion runs by default unless **`--no-promote`**). |
61
+ | `--no-promote` | After **`run synthesis`** or **`analysis`**: skip copying **`.gluecharm/context`** into the repo for this run. |
62
62
  | `--help`, `-h` | Show built-in help (also `help` as first positional). Does **not** create `.easyspecs/config.json`. |
63
63
  | `--version`, `-V` | Print CLI package version (also `version` as first positional). Does **not** create config. |
64
64
  | `--config <path>` | Parsed but **unused** by `main.ts` today; configuration is read from `<repoRoot>/.easyspecs/config.json`. |
@@ -148,7 +148,7 @@ Each row lists **command-specific CLI tokens**, then **what configuration applie
148
148
  | `auth status` | — | Reads session file. |
149
149
  | `run synthesis` | — | Single SRS-9 context pass (same as extension **Run Analysis**). `**requireOpenCode`** → executable + credentials unless skip flag. Uses `**merged.pipelineOpenCode**` from `**config.json**`. `**--promote` / `--no-promote**` controls promotion after success. |
150
150
  | `run synthesis resume-missing`, `run synthesis resume-synthesis` | Optional `**--worktree <path>**` | Same implementation: resolves checkout via `--worktree` or stored snapshot (`[resolveAdHocCheckoutRoot](../../src/cli/main.ts)`). OpenCode + `**merged.pipelineOpenCode**`. |
151
- | `analysis` | Optional tail: `**--force-new-context-analysis**`. **`--upload`** → usage exit. **`--synthesis-only`**: accepted as a **hidden** Commander option (legacy scripts); **ignored** with one stderr **`[deprecated]`** line (**SRS-60**). | **SRS-32** full **Factory** loop: synthesis convergence (until no missing artefacts), coverage, zero-ref, report, **link mapping**, index assembly; **Phase 7** backend sync is skipped in CLI (**`skipBackendSync`**). Push context with **`upload context`** / **`upload republish`** after analysis. Factory timing from `**merged.factory`** + `**--ci**` defaults. Debug: `**easyspecs.factory.debug**`. OpenCode options from `**merged.pipelineOpenCode**`. **SRS-46:** if `**easyspecs.factory.cloudContextAnalyzed`** is `**true**` and `**--force-new-context-analysis**` is absent, exits **0** without running the Factory (`**analysisSkipped**`, …). **SRS-59:** stale worktree registration recovery — see **§ Analysis git checkout** below. |
151
+ | `analysis` | Optional tail: `**--force-new-context-analysis**`. **`--upload`** → usage exit. **`--synthesis-only`**: accepted as a **hidden** Commander option (legacy scripts); **ignored** with one stderr **`[deprecated]`** line (**SRS-60**). | **SRS-32** full **Factory** loop: synthesis convergence (until no missing artefacts), coverage, zero-ref, report, **link mapping**, index assembly; **Phase 7** backend sync is skipped in CLI (**`skipBackendSync`**). **On success:** always promotes **`.gluecharm/context`** from the analysis worktree into **`<repoRoot>`** unless **`--no-promote`** (stderr **`[pipeline:analysis]`**); **`easyspecs.analysis.promoteContextToWorkspace`** does **not** apply to this step. Push context with **`upload context`** / **`upload republish`** after analysis if you need cloud sync. Factory timing from `**merged.factory`** + `**--ci**` defaults. Debug: `**easyspecs.factory.debug**`. OpenCode options from `**merged.pipelineOpenCode**`. **SRS-46:** if `**easyspecs.factory.cloudContextAnalyzed`** is `**true**` and `**--force-new-context-analysis**` is absent, exits **0** without running the Factory (`**analysisSkipped**`, …). **SRS-59:** stale worktree registration recovery — see **§ Analysis git checkout** below. |
152
152
  | `update context` | — (no subcommand flags; **SRS-60** removed `**--upload**`) | **SRS-55:** incremental context refresh — baseline → git delta → worktree → `**changes-since-date.md`** → optional remediation → **`--promote` / `--no-promote**` (global) → persist `**lastRunAt**`. Run **`upload context`** separately if you need cloud sync. |
153
153
  | `diagnose reference-coverage` | `**--root workspace**` | `**--root worktree**`, optional `**--worktree <path>**` | Gate when `**easyspecs.diagnose.zeroReference.maxPercentNonReferenced**` is a finite number (not `**null**`): failure when metric exceeds it. |
154
154
  | `diagnose coordination-duplicates` | Same `**--root**` / `**--worktree**` | `**easyspecs.diagnose.coordinationDuplicates.strict**`: when `**true**` (default), duplicate/orphan issues fail the exit code. |
package/dist/main.cjs CHANGED
@@ -10703,7 +10703,7 @@ var {
10703
10703
  function createEasyspecsCliProgram() {
10704
10704
  const program2 = new Command();
10705
10705
  program2.name("easyspecs-cli").allowExcessArguments(false).enablePositionalOptions();
10706
- program2.option("--cwd <dir>", "Repository root (default: current directory)").option("--ci", "Non-interactive; fail fast if configuration is missing").option("--json", "Print one JSON summary line on stdout").option("--verbose", "Extra stderr logging").option("--config <path>", "Configuration file path (accepted; currently unused)").option("--api-base-url <url>", "System Manager API origin (overrides config)").option("--session-path <file>", "Session JSON for this run (overrides config)").option("--environment <environment>", "Effective deployment (alias: --env)").option("--env <environment>", "Alias for --environment").option("--promote", "Copy generated context into workspace after synthesis").option("--no-promote", "Disable promotion of generated context into workspace");
10706
+ program2.option("--cwd <dir>", "Repository root (default: current directory)").option("--ci", "Non-interactive; fail fast if configuration is missing").option("--json", "Print one JSON summary line on stdout").option("--verbose", "Extra stderr logging").option("--config <path>", "Configuration file path (accepted; currently unused)").option("--api-base-url <url>", "System Manager API origin (overrides config)").option("--session-path <file>", "Session JSON for this run (overrides config)").option("--environment <environment>", "Effective deployment (alias: --env)").option("--env <environment>", "Alias for --environment").option("--promote", "After run synthesis: force context promotion when config would disable it").option("--no-promote", "After run synthesis or analysis: skip copying .gluecharm/context into repo for this run");
10707
10707
  program2.command("help").description("Show help");
10708
10708
  program2.command("version").description("Print CLI version");
10709
10709
  program2.command("doctor").description("Check readiness and/or inspect configuration").option("--readiness", "Print readiness summary (default)").option("--inspect-config", "Print redacted merged configuration");
@@ -21119,13 +21119,28 @@ async function runGenerateContextFactory(deps) {
21119
21119
  };
21120
21120
  await Promise.resolve(deps.post(payload));
21121
21121
  };
21122
+ const logFactoryEnd = (r) => {
21123
+ const log = deps.factoryLog;
21124
+ if (!log) {
21125
+ return;
21126
+ }
21127
+ const tag = r.cancelled === true ? "cancelled" : r.ok ? "succeeded" : "failed";
21128
+ const tail = r.message ? ` \u2014 ${r.message}` : "";
21129
+ log(`[factory] Generate Context factory ended (${tag}) \u2014 ${String(r.totalElapsedMs)}ms${tail}`);
21130
+ };
21131
+ const fin = (r) => {
21132
+ logFactoryEnd(r);
21133
+ return r;
21134
+ };
21122
21135
  const fail = async (message) => {
21123
21136
  await post();
21124
21137
  let ff = buildFactoryFailuresFromRows(phases);
21125
21138
  if (ff.length === 0) {
21126
21139
  ff = syntheticUnknownFactoryFailure(message);
21127
21140
  }
21128
- return { ok: false, message, totalElapsedMs: macroEnd(), factoryFailures: ff };
21141
+ const r = { ok: false, message, totalElapsedMs: macroEnd(), factoryFailures: ff };
21142
+ logFactoryEnd(r);
21143
+ return r;
21129
21144
  };
21130
21145
  const pipelineCtx = {
21131
21146
  signal: deps.signal,
@@ -21140,7 +21155,7 @@ async function runGenerateContextFactory(deps) {
21140
21155
  if (isAborted(signal)) {
21141
21156
  setRowTimed("synthesis_convergence", "cancelled", "Stopped.");
21142
21157
  await post();
21143
- return { ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() };
21158
+ return fin({ ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() });
21144
21159
  }
21145
21160
  let delayMs = config.initialDelayMs;
21146
21161
  let outerIter = 0;
@@ -21149,7 +21164,7 @@ async function runGenerateContextFactory(deps) {
21149
21164
  if (isAborted(signal)) {
21150
21165
  setRowTimed("synthesis_convergence", "cancelled");
21151
21166
  await post();
21152
- return { ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() };
21167
+ return fin({ ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() });
21153
21168
  }
21154
21169
  if (!resumeSynthesis) {
21155
21170
  setRowTimed("create_analysis_worktree", "running", "Creating git analysis worktree\u2026");
@@ -21166,7 +21181,7 @@ async function runGenerateContextFactory(deps) {
21166
21181
  setRowTimed("materialize_opencode_agents", "cancelled");
21167
21182
  setRowTimed("synthesis_convergence", "cancelled");
21168
21183
  await post();
21169
- return { ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() };
21184
+ return fin({ ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() });
21170
21185
  }
21171
21186
  setRowTimed("materialize_opencode_agents", "running", "Materializing OpenCode agents\u2026");
21172
21187
  await post();
@@ -21199,7 +21214,7 @@ async function runGenerateContextFactory(deps) {
21199
21214
  if (isAborted(signal)) {
21200
21215
  setRowTimed("synthesis_convergence", "cancelled");
21201
21216
  await post();
21202
- return { ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() };
21217
+ return fin({ ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() });
21203
21218
  }
21204
21219
  setRowTimed(
21205
21220
  "synthesis_convergence",
@@ -21212,7 +21227,7 @@ async function runGenerateContextFactory(deps) {
21212
21227
  if (syn.cancelled || isAborted(signal)) {
21213
21228
  setRowTimed("synthesis_convergence", "cancelled");
21214
21229
  await post();
21215
- return { ok: false, cancelled: true, message: syn.message ?? "Synthesis cancelled.", totalElapsedMs: macroEnd() };
21230
+ return fin({ ok: false, cancelled: true, message: syn.message ?? "Synthesis cancelled.", totalElapsedMs: macroEnd() });
21216
21231
  }
21217
21232
  if (!syn.ok) {
21218
21233
  setRowTimed("synthesis_convergence", "failed", syn.message ?? "Synthesis failed.");
@@ -21246,7 +21261,7 @@ async function runGenerateContextFactory(deps) {
21246
21261
  } catch {
21247
21262
  setRowTimed("synthesis_convergence", "cancelled");
21248
21263
  await post();
21249
- return { ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() };
21264
+ return fin({ ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() });
21250
21265
  }
21251
21266
  delayMs = computeNextBackoffDelay(delayMs, config.backoffMultiplier, config.maxDelayMs);
21252
21267
  setRowTimed("synthesis_convergence", "pending", "Next pass: resume parallel pool on same checkout\u2026");
@@ -21256,12 +21271,12 @@ async function runGenerateContextFactory(deps) {
21256
21271
  setRowTimed("synthesis_convergence", "succeeded", "No missing artefacts.");
21257
21272
  await post();
21258
21273
  if (config.synthesisOnly) {
21259
- return { ok: true, message: "Synthesis-only analysis complete.", totalElapsedMs: macroEnd() };
21274
+ return fin({ ok: true, message: "Synthesis-only analysis complete.", totalElapsedMs: macroEnd() });
21260
21275
  }
21261
21276
  if (isAborted(signal)) {
21262
21277
  setRowTimed("reference_coverage", "cancelled");
21263
21278
  await post();
21264
- return { ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() };
21279
+ return fin({ ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() });
21265
21280
  }
21266
21281
  setRowTimed("reference_coverage", "running");
21267
21282
  bumpPhaseRunCount("reference_coverage");
@@ -21277,7 +21292,7 @@ async function runGenerateContextFactory(deps) {
21277
21292
  if (isAborted(signal)) {
21278
21293
  setRowTimed("zero_reference_remediation_convergence", "cancelled");
21279
21294
  await post();
21280
- return { ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() };
21295
+ return fin({ ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() });
21281
21296
  }
21282
21297
  let remDelay = config.synthesisRemediationShareBackoff ? delayMs : config.initialDelayMs;
21283
21298
  let remIter = 0;
@@ -21285,7 +21300,7 @@ async function runGenerateContextFactory(deps) {
21285
21300
  if (isAborted(signal)) {
21286
21301
  setRowTimed("zero_reference_remediation_convergence", "cancelled");
21287
21302
  await post();
21288
- return { ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() };
21303
+ return fin({ ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() });
21289
21304
  }
21290
21305
  setRowTimed("zero_reference_remediation_convergence", "running");
21291
21306
  bumpPhaseRunCount("zero_reference_remediation_convergence");
@@ -21294,7 +21309,7 @@ async function runGenerateContextFactory(deps) {
21294
21309
  if (rem.cancelled || isAborted(signal)) {
21295
21310
  setRowTimed("zero_reference_remediation_convergence", "cancelled");
21296
21311
  await post();
21297
- return { ok: false, cancelled: true, message: rem.message ?? "Remediation cancelled.", totalElapsedMs: macroEnd() };
21312
+ return fin({ ok: false, cancelled: true, message: rem.message ?? "Remediation cancelled.", totalElapsedMs: macroEnd() });
21298
21313
  }
21299
21314
  if (!rem.ok) {
21300
21315
  setRowTimed("zero_reference_remediation_convergence", "failed", rem.message);
@@ -21328,7 +21343,7 @@ async function runGenerateContextFactory(deps) {
21328
21343
  } catch {
21329
21344
  setRowTimed("zero_reference_remediation_convergence", "cancelled");
21330
21345
  await post();
21331
- return { ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() };
21346
+ return fin({ ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() });
21332
21347
  }
21333
21348
  remDelay = computeNextBackoffDelay(remDelay, config.backoffMultiplier, config.maxDelayMs);
21334
21349
  }
@@ -21337,7 +21352,7 @@ async function runGenerateContextFactory(deps) {
21337
21352
  if (isAborted(signal)) {
21338
21353
  setRowTimed("link_mapping_pipeline", "cancelled");
21339
21354
  await post();
21340
- return { ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() };
21355
+ return fin({ ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() });
21341
21356
  }
21342
21357
  setRowTimed("link_mapping_pipeline", "running", "Refreshing link navigation after remediation\u2026");
21343
21358
  bumpPhaseRunCount("link_mapping_pipeline");
@@ -21353,7 +21368,7 @@ async function runGenerateContextFactory(deps) {
21353
21368
  if (isAborted(signal)) {
21354
21369
  setRowTimed("reference_coverage", "cancelled");
21355
21370
  await post();
21356
- return { ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() };
21371
+ return fin({ ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() });
21357
21372
  }
21358
21373
  setRowTimed("reference_coverage", "running", "Refreshing coverage after remediation\u2026");
21359
21374
  bumpPhaseRunCount("reference_coverage");
@@ -21392,7 +21407,7 @@ async function runGenerateContextFactory(deps) {
21392
21407
  if (isAborted(signal)) {
21393
21408
  setRowTimed("reference_coverage_execution_report", "cancelled");
21394
21409
  await post();
21395
- return { ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() };
21410
+ return fin({ ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() });
21396
21411
  }
21397
21412
  setRowTimed("reference_coverage_execution_report", "running");
21398
21413
  bumpPhaseRunCount("reference_coverage_execution_report");
@@ -21401,7 +21416,7 @@ async function runGenerateContextFactory(deps) {
21401
21416
  if (rep.cancelled || isAborted(signal)) {
21402
21417
  setRowTimed("reference_coverage_execution_report", "cancelled", rep.message);
21403
21418
  await post();
21404
- return { ok: false, cancelled: true, message: rep.message ?? "Factory stopped.", totalElapsedMs: macroEnd() };
21419
+ return fin({ ok: false, cancelled: true, message: rep.message ?? "Factory stopped.", totalElapsedMs: macroEnd() });
21405
21420
  }
21406
21421
  if (!rep.ok) {
21407
21422
  setRowTimed("reference_coverage_execution_report", "failed", rep.message);
@@ -21413,7 +21428,7 @@ async function runGenerateContextFactory(deps) {
21413
21428
  if (isAborted(signal)) {
21414
21429
  setRowTimed("link_mapping_pipeline", "cancelled");
21415
21430
  await post();
21416
- return { ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() };
21431
+ return fin({ ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() });
21417
21432
  }
21418
21433
  setRowTimed("link_mapping_pipeline", "running");
21419
21434
  bumpPhaseRunCount("link_mapping_pipeline");
@@ -21429,7 +21444,7 @@ async function runGenerateContextFactory(deps) {
21429
21444
  if (isAborted(signal)) {
21430
21445
  setRowTimed("assemble_application_context_index", "cancelled");
21431
21446
  await post();
21432
- return { ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() };
21447
+ return fin({ ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() });
21433
21448
  }
21434
21449
  setRowTimed("assemble_application_context_index", "running");
21435
21450
  bumpPhaseRunCount("assemble_application_context_index");
@@ -21445,7 +21460,7 @@ async function runGenerateContextFactory(deps) {
21445
21460
  if (isAborted(signal)) {
21446
21461
  setRowTimed("backend_context_sync", "cancelled");
21447
21462
  await post();
21448
- return { ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() };
21463
+ return fin({ ok: false, cancelled: true, message: "Factory stopped.", totalElapsedMs: macroEnd() });
21449
21464
  }
21450
21465
  setRowTimed("backend_context_sync", "running");
21451
21466
  bumpPhaseRunCount("backend_context_sync");
@@ -21458,7 +21473,7 @@ async function runGenerateContextFactory(deps) {
21458
21473
  }
21459
21474
  setRowTimed("backend_context_sync", "succeeded", up.message);
21460
21475
  await post();
21461
- return { ok: true, message: "Analysis complete.", totalElapsedMs: macroEnd() };
21476
+ return fin({ ok: true, message: "Analysis complete.", totalElapsedMs: macroEnd() });
21462
21477
  } catch (e) {
21463
21478
  const msg = e instanceof Error ? e.message : String(e);
21464
21479
  return fail(msg);
@@ -24500,6 +24515,7 @@ function buildFactoryDepsHeadless(input) {
24500
24515
  };
24501
24516
  return {
24502
24517
  signal,
24518
+ factoryLog: log,
24503
24519
  config: { ...macroConfig, ...input.synthesisOnly ? { synthesisOnly: true } : {} },
24504
24520
  sleep: (ms) => sleepUntilAborted(ms, signal),
24505
24521
  post: (payload) => {
@@ -28499,6 +28515,27 @@ async function main() {
28499
28515
  skipBackendSync: true
28500
28516
  });
28501
28517
  const res = await runGenerateContextFactory(deps);
28518
+ if (res.ok && res.cancelled !== true && flags.promote !== false) {
28519
+ const snap = readAnalysisWorkspaceSnapshot(storage);
28520
+ const wt = snap?.adHocWorktreePath?.trim();
28521
+ if (wt) {
28522
+ const sourceCtx = path59.join(wt, ".gluecharm", "context");
28523
+ if (fs59.existsSync(sourceCtx)) {
28524
+ const n = promoteContextDirectoryToWorkspaceFs(sourceCtx, repoRoot);
28525
+ logErr(flags, `[pipeline:analysis] promoted ${String(n.filesCopied)} file(s) \u2192 ${repoRoot}`);
28526
+ } else {
28527
+ logErr(flags, `[pipeline:analysis] promote skipped: missing ${sourceCtx}`);
28528
+ }
28529
+ } else {
28530
+ logErr(flags, "[pipeline:analysis] promote skipped: no adHocWorktreePath in workspace snapshot");
28531
+ }
28532
+ } else if (res.ok && res.cancelled !== true && flags.promote === false) {
28533
+ logErr(flags, "[pipeline:analysis] promotion skipped (--no-promote)");
28534
+ }
28535
+ {
28536
+ const outcome = res.cancelled === true ? "cancelled" : res.ok ? "ok" : "failed";
28537
+ logErr(flags, `[pipeline:analysis] easyspecs-cli analysis finished (${outcome}) \u2014 ${(/* @__PURE__ */ new Date()).toISOString()}`);
28538
+ }
28502
28539
  const analysisEnvelope = {
28503
28540
  ok: res.ok,
28504
28541
  cancelled: res.cancelled,