fullstackgtm 0.48.0 → 0.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/CONTRIBUTING.md +23 -4
  3. package/DATA-FLOWS.md +7 -2
  4. package/INSTALL_FOR_AGENTS.md +13 -4
  5. package/README.md +12 -1
  6. package/SECURITY.md +27 -3
  7. package/dist/cli/audit.js +10 -7
  8. package/dist/cli/auth.js +8 -4
  9. package/dist/cli/fix.js +20 -7
  10. package/dist/cli/help.js +10 -6
  11. package/dist/cli/market.js +180 -2
  12. package/dist/cli/plans.js +56 -5
  13. package/dist/cli/ui.js +2 -0
  14. package/dist/cli.js +1 -1
  15. package/dist/config.d.ts +13 -1
  16. package/dist/config.js +23 -2
  17. package/dist/connectors/prospectSources.js +4 -11
  18. package/dist/connectors/salesforce.js +12 -5
  19. package/dist/connectors/salesforceAuth.d.ts +9 -0
  20. package/dist/connectors/salesforceAuth.js +47 -5
  21. package/dist/connectors/theirstack.d.ts +0 -19
  22. package/dist/connectors/theirstack.js +3 -10
  23. package/dist/credentials.js +36 -26
  24. package/dist/index.d.ts +3 -2
  25. package/dist/index.js +3 -2
  26. package/dist/market.d.ts +1 -1
  27. package/dist/market.js +7 -127
  28. package/dist/marketClassify.d.ts +10 -0
  29. package/dist/marketClassify.js +52 -1
  30. package/dist/marketSourcing.js +7 -45
  31. package/dist/mcp.js +67 -115
  32. package/dist/planStore.d.ts +28 -1
  33. package/dist/planStore.js +195 -49
  34. package/dist/providerError.d.ts +21 -0
  35. package/dist/providerError.js +30 -0
  36. package/dist/publicHttp.d.ts +28 -0
  37. package/dist/publicHttp.js +143 -0
  38. package/dist/secureFile.d.ts +15 -0
  39. package/dist/secureFile.js +164 -0
  40. package/dist/types.d.ts +1 -1
  41. package/docs/api.md +29 -2
  42. package/docs/architecture.md +13 -2
  43. package/llms.txt +7 -0
  44. package/package.json +5 -3
  45. package/skills/fullstackgtm/SKILL.md +7 -1
  46. package/src/cli/audit.ts +10 -7
  47. package/src/cli/auth.ts +8 -4
  48. package/src/cli/fix.ts +28 -7
  49. package/src/cli/help.ts +10 -6
  50. package/src/cli/market.ts +181 -2
  51. package/src/cli/plans.ts +66 -5
  52. package/src/cli/ui.ts +1 -0
  53. package/src/cli.ts +1 -1
  54. package/src/config.ts +39 -1
  55. package/src/connectors/prospectSources.ts +4 -11
  56. package/src/connectors/salesforce.ts +12 -5
  57. package/src/connectors/salesforceAuth.ts +46 -6
  58. package/src/connectors/theirstack.ts +3 -10
  59. package/src/credentials.ts +47 -28
  60. package/src/index.ts +4 -0
  61. package/src/market.ts +7 -111
  62. package/src/marketClassify.ts +61 -1
  63. package/src/marketSourcing.ts +7 -43
  64. package/src/mcp.ts +93 -136
  65. package/src/planStore.ts +235 -57
  66. package/src/providerError.ts +37 -0
  67. package/src/publicHttp.ts +147 -0
  68. package/src/secureFile.ts +169 -0
  69. package/src/types.ts +1 -0
package/CHANGELOG.md CHANGED
@@ -7,6 +7,44 @@ The path to 1.0 is planned in [docs/roadmap-to-1.0.md](https://github.com/fullst
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.49.0] — 2026-07-10
11
+
12
+ ### Security
13
+
14
+ - MCP apply now accepts only store-backed, HMAC-approved plan ids; external
15
+ plan paths and caller-supplied approvals/value overrides are rejected.
16
+ - Apply uses an atomic single-owner claim and durable attempt journal.
17
+ Uncertain attempts remain fail-closed until an operator acknowledges provider
18
+ state; recovery clears every approval and never replays writes automatically.
19
+ - Salesforce credential-bearing endpoints are restricted to canonical HTTPS
20
+ Salesforce origins and redirects are handled without forwarding credentials.
21
+ - Executable rule packages require explicit CLI trust and are disabled entirely
22
+ in MCP. Implicit repository configuration cannot activate plugins.
23
+ - Sensitive local files use atomic no-follow writes and verified no-follow
24
+ reads; symlinked credential, plan, and managed-home paths are refused.
25
+ - Market capture/sourcing uses DNS-pinned public-only HTTP with per-hop redirect
26
+ validation, bounded responses, and cross-origin credential stripping.
27
+ - Provider HTTP failures expose typed status metadata without raw response
28
+ bodies, reflected secrets, or PII.
29
+
30
+ ### Changed
31
+
32
+ - **Breaking pre-1.0 plan-store contract:** `PlanStore` implementations now
33
+ participate in apply claims, attempt recovery, and claim-bound run recording;
34
+ `ApprovalStatus` includes `applying`. Custom stores must implement the new
35
+ lifecycle before upgrading.
36
+ - Public mirror generation is commit-derived, allowlisted, secret-scanned, and
37
+ fail-closed. npm publishing uses pinned actions/npm plus a protected release
38
+ environment and provenance verification.
39
+ - Package CI now validates Node 20/22, Linux/macOS/Windows compiled runtimes,
40
+ and actual installed tarballs with and without optional MCP peers.
41
+
42
+ ### Fixed
43
+
44
+ - `bulk-update --help` and `-h` now return focused help before parsing an object
45
+ type or touching configuration, credentials, disk, or network.
46
+ - Installation guidance no longer pins a brittle exact demo finding count.
47
+
10
48
  ## [0.48.0] — 2026-07-08
11
49
 
12
50
  ### Added
package/CONTRIBUTING.md CHANGED
@@ -44,6 +44,21 @@ node --experimental-strip-types --test tests/fullstackgtm*.test.ts # from the
44
44
  node packages/fullstackgtm/src/bin.ts doctor # run the CLI locally
45
45
  ```
46
46
 
47
+ Before a release-facing change, verify the artifact rather than only its source
48
+ tree:
49
+
50
+ ```bash
51
+ cd packages/fullstackgtm
52
+ npm run build
53
+ node scripts/verify-packed-install.mjs # isolated install, optional peers absent
54
+ node scripts/verify-packed-install.mjs --with-peers # isolated install, MCP peers present
55
+ ```
56
+
57
+ The verifier creates the real npm tarball, enforces required/prohibited paths,
58
+ imports the public export, checks its declaration entrypoint, and runs both
59
+ installed binaries. CI repeats compiled-runtime checks on Node 20 and 22 across
60
+ Linux, macOS, and Windows; source-level tests remain on Node 22.6+.
61
+
47
62
  > **Tests live at the monorepo root `tests/`, not in the package.** Running
48
63
  > `npm test` *inside* `packages/fullstackgtm` deliberately fails with a pointer
49
64
  > (a `pretest` guard) rather than silently passing with zero tests. On the
@@ -78,13 +93,17 @@ the development monorepo). From the monorepo:
78
93
 
79
94
  1. Bump `packages/fullstackgtm/package.json` version + add a `CHANGELOG.md`
80
95
  entry; merge to `main`.
81
- 2. `scripts/sync-oss.sh --push` (builds a fresh `dist/`, mirrors the package +
82
- rewritten tests to `fullstackgtm/core`).
96
+ 2. Run `scripts/sync-oss.sh` to export an allowlisted mirror from the full
97
+ source commit, build `dist/`, scan for secrets, and inspect the complete
98
+ staged diff. Then rerun with `--push` after review. Clone failures stop the
99
+ release; only a genuine first publication may use the separately confirmed
100
+ `--initialize-public-repo` mode.
83
101
  3. Re-check the mirror's `package.json` version, then tag `vX.Y.Z` on the mirror
84
102
  and push the tag.
85
103
  4. GitHub Actions (`release.yml`) publishes to npm via **OIDC trusted
86
- publishing** — no tokens. It rebuilds from source and refuses to publish if
87
- the committed `dist/` differs from a fresh build (supply-chain gate).
104
+ publishing** — no tokens. The protected `npm-production` environment must
105
+ require an independent reviewer. The workflow rebuilds from source and
106
+ refuses to publish if committed `dist/` differs (supply-chain gate).
88
107
 
89
108
  **Access a new co-maintainer needs to cut a release:** write access to
90
109
  `github.com/fullstackgtm/core` (for `sync-oss.sh --push` and the tag push). No
package/DATA-FLOWS.md CHANGED
@@ -10,7 +10,8 @@ is no fullstackgtm-operated server in the data path for the open package.**
10
10
 
11
11
  - CRM snapshots, patch plans, approvals, apply-run records, market captures and
12
12
  observations, enrich run state, and the signing/credential stores all live
13
- under `$FSGTM_HOME` (default `~/.fullstackgtm`), `0600`/`0700`. Nothing is
13
+ under `$FSGTM_HOME` (default `~/.fullstackgtm`), `0600`/`0700`, using atomic
14
+ no-follow file I/O. Nothing is
14
15
  uploaded to Full Stack GTM.
15
16
  - No telemetry, analytics, or phone-home. The core package has zero runtime
16
17
  dependencies; the only network calls are the ones listed below, all to
@@ -23,12 +24,16 @@ is no fullstackgtm-operated server in the data path for the open package.**
23
24
  | `snapshot`, `audit`, `apply`, `resolve`, `bulk-update`, `dedupe`, `reassign`, `fix`, `enrich` (writeback) | **Your CRM** (HubSpot / Salesforce / Stripe API) | Reads: your CRM records. Writes: only approved patch operations. | Your CRM token (env / stored / broker) |
24
25
  | `call parse`, `call score`, `market classify`, `market refresh` | **Your LLM provider** (api.anthropic.com or api.openai.com) | The call transcript / captured competitor page text you point at, plus the extraction prompt | Your `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` (BYO) |
25
26
  | `enrich append --source apollo`, `enrich refresh` | **Apollo** (api.apollo.io) | The company domain / contact email being enriched | Your `APOLLO_API_KEY` (BYO) |
26
- | `market capture`, `market refresh` | **Public vendor websites** you list in `market.config.json` | An HTTP GET (no data sent beyond the request); SSRF-guarded to public hosts only | none |
27
+ | `market capture`, `market refresh` | **Public vendor websites** you list in `market.config.json` | A bounded HTTP GET; DNS answers are validated and pinned, mixed/private/reserved answers are refused, and every redirect is revalidated | none |
27
28
  | `login --via <url>` (optional) | **Your hosted deployment's broker** | A pairing handshake; the broker mints short-lived CRM tokens | broker pairing token |
28
29
 
29
30
  Commands not listed (`plans`, `rules`, `doctor`, `schedule`, `audit-log`,
30
31
  `diff`, `merge`, report rendering) make **no network calls**.
31
32
 
33
+ Provider HTTP failures persist only typed provider/operation/status/retryability
34
+ metadata. Raw third-party response bodies—which may reflect credentials,
35
+ filters, or PII—are not written to run records or normal CLI/MCP errors.
36
+
32
37
  ## Avoiding third-party data egress
33
38
 
34
39
  - **LLM verbs are optional.** `call parse --deterministic` uses a free,
@@ -33,9 +33,11 @@ machine — that is normal and does not block the next step.
33
33
  fullstackgtm audit --demo --json
34
34
  ```
35
35
 
36
- Expect a JSON patch plan with `dryRun: true` and 110 findings/110 operations over a generated,
37
- deliberately messy CRM. Deterministic per seed: `--seed 7` is the default, so
38
- two runs produce identical finding and operation ids. Exit code 0.
36
+ Expect a JSON patch plan with `dryRun: true`, a non-empty `findings` array, and
37
+ matching proposed operations over a generated, deliberately messy CRM. The
38
+ exact count can grow as built-in rules are added; the stable contract is
39
+ determinism per release and seed. `--seed 7` is the default, so two runs on the
40
+ same version produce identical finding and operation ids. Exit code 0.
39
41
 
40
42
  This proves the whole pipeline (snapshot → audit → plan) without any account.
41
43
 
@@ -133,10 +135,16 @@ works from inside existing projects too.
133
135
  Tools exposed over stdio — read-only: `fullstackgtm_audit`,
134
136
  `fullstackgtm_capabilities`, `fullstackgtm_rules`, `fullstackgtm_suggest`,
135
137
  `fullstackgtm_call_parse`, `fullstackgtm_resolve`, `fullstackgtm_market_worksheet`. Gated:
136
- `fullstackgtm_apply` (requires explicit `approvedOperationIds`),
138
+ `fullstackgtm_apply` (stored `planId` only; approvals and values must already be
139
+ human-approved and HMAC-signed in the local plan store),
137
140
  `fullstackgtm_market_observe` (every quoted span is verified against the
138
141
  stored captures before anything is appended).
139
142
 
143
+ MCP cannot execute rule packages, apply external plan files, or supply its own
144
+ approvals/value overrides. For an interrupted apply, reconcile provider state,
145
+ then run `fullstackgtm plans recover <id> --acknowledge-uncertain-writes`; this
146
+ replays nothing and clears approvals so a fresh review is required.
147
+
140
148
  ## Troubleshooting
141
149
 
142
150
  | Symptom | Fix |
@@ -145,3 +153,4 @@ stored captures before anything is appended).
145
153
  | `No HubSpot credentials` (exit 1) | Set `HUBSPOT_ACCESS_TOKEN` or have a human run `fullstackgtm login hubspot --hosted` |
146
154
  | MCP server prints peer-dependency help | Install `@modelcontextprotocol/sdk` and `zod` (see step 6) |
147
155
  | Need machine state for orchestration | `fullstackgtm doctor --json` |
156
+ | Plan is `applying` after an interrupted request | Reconcile provider state, then run `fullstackgtm plans recover <id> --acknowledge-uncertain-writes`; review and approve again |
package/README.md CHANGED
@@ -373,6 +373,12 @@ fullstackgtm logout hubspot # or: salesforce | broker
373
373
 
374
374
  BYO direct credentials always win over a hosted broker pairing, so an operator can override the team default. First-party app secrets never ship in the npm package; hosted login stores a local broker token and mints provider tokens server-side. HubSpot does not support the device-authorization grant or secretless public clients, which is why the bring-your-own-app OAuth path requires client credentials; they are stored locally for silent refresh, the same model as `gcloud` and `aws` CLI profiles.
375
375
 
376
+ Salesforce credential-bearing URLs are restricted to canonical HTTPS
377
+ Salesforce-owned production, sandbox, and My Domain origins. Lookalike hosts,
378
+ userinfo, paths/query/fragment, nonstandard ports, unsafe token-response
379
+ instance URLs, and credential-forwarding redirects are refused both when the
380
+ credential is stored and every time it is used.
381
+
376
382
  ## Connect your CRM
377
383
 
378
384
  What each provider actually requires before `audit --provider <name>` works on your data.
@@ -539,10 +545,15 @@ Nine tools are exposed over stdio.
539
545
 
540
546
  **Read-only:** `fullstackgtm_audit` (sample, demo, file, or live provider sources with optional rule scoping), `fullstackgtm_capabilities` (server/tool capability manifest), `fullstackgtm_rules` (rule discovery), `fullstackgtm_suggest` (deterministic placeholder values with confidence + reasons), `fullstackgtm_call_parse` (transcripts → provenance-marked segments, insights, and evidence), `fullstackgtm_resolve` (the create gate: exists / ambiguous / safe_to_create), and `fullstackgtm_market_worksheet` (the classification packet for one vendor: claims, judging rules, captured page texts).
541
547
 
542
- **Gated:** `fullstackgtm_apply` (requires explicit `approvedOperationIds`; placeholders still need value overrides) and `fullstackgtm_market_observe` (verifies every quoted span against the stored captures before appending — nothing is stored unless the whole set passes).
548
+ **Gated:** `fullstackgtm_apply` (accepts only a stored plan id; approvals and values must already be human-approved and HMAC-signed in the plan store) and `fullstackgtm_market_observe` (verifies every quoted span against the stored captures before appending — nothing is stored unless the whole set passes).
543
549
 
544
550
  Tokens stored via `fullstackgtm login` are picked up automatically — the env var is only needed when no stored login exists.
545
551
 
552
+ Rule packages are executable JavaScript and are disabled by default. In the CLI,
553
+ review the modules and use `--config <path> --allow-plugins`; use `--no-plugins`
554
+ to apply only declarative config. MCP never executes rule packages: mutable
555
+ tool-call paths are not a durable code-trust boundary.
556
+
546
557
  ## Safety model
547
558
 
548
559
  1. Reads are safe by default; audits never mutate anything.
package/SECURITY.md CHANGED
@@ -21,7 +21,10 @@ newest version, not backported (the project is pre-1.0).
21
21
  **Credentials.** API tokens are never accepted as command-line arguments
22
22
  (they would leak into the process table and shell history); they come from an
23
23
  environment variable or stdin only, and are stored `0600` under a `0700` home
24
- (`$FSGTM_HOME`, default `~/.fullstackgtm`), re-tightened on read. This is the
24
+ (`$FSGTM_HOME`, default `~/.fullstackgtm`). Writes use exclusive temporary
25
+ files, fsync, and atomic rename; reads require regular files opened without
26
+ following symlinks. Managed-home, credential, and plan-store symlink paths are
27
+ refused before chmod/read/write. This is the
25
28
  same custody model as the `gcloud`/`aws` CLIs. The hosted broker
26
29
  (`login --via`) exists so a team can connect a CRM once, server-side, and hand
27
30
  laptops only a revocable pairing token instead of a long-lived super-admin key;
@@ -57,13 +60,34 @@ approved on one machine cannot be applied on another (the key does not travel).
57
60
  Documented boundary: this defends the plan file, not an attacker who already
58
61
  holds the signing key (same directory and permissions as the credential store).
59
62
 
63
+ **Single-owner apply and recovery.** Store-backed CLI and MCP writes atomically
64
+ claim an approved plan and journal the provider, caller surface, and attempt
65
+ state before provider I/O. A second process cannot apply the same approval.
66
+ Failures after provider I/O remain `applying`/uncertain because a timeout cannot
67
+ prove no remote write landed. Recovery never auto-replays: after reconciling CRM
68
+ state, `plans recover <id> --acknowledge-uncertain-writes` clears every approval
69
+ and signature and requires a fresh human review.
70
+
71
+ **Executable configuration.** `rulePackages` are arbitrary JavaScript. An
72
+ implicitly discovered repository config cannot activate them; CLI execution
73
+ requires explicit `--config <path> --allow-plugins` (`--no-plugins` disables
74
+ them). MCP never executes rule packages or accepts external plans/caller
75
+ approvals.
76
+
77
+ **Credential origins.** Salesforce access and refresh tokens are sent only to
78
+ canonical HTTPS Salesforce-owned origins. Userinfo, paths/query/fragment,
79
+ nonstandard ports, lookalike domains, unsafe token-response instance URLs, and
80
+ credential-preserving redirects are refused at input, storage, and use.
81
+
60
82
  **Scheduling never auto-approves.** Scheduled (cron) runs are restricted to a
61
83
  read/plan-side allowlist plus `apply --plan-id` whose approved status and
62
84
  signatures are re-checked at every firing. Arbitrary shell is not schedulable.
63
85
 
64
86
  **Untrusted input.** Competitor pages fetched by `market capture` are guarded
65
- against SSRF (scheme allowlist; private/loopback/link-local/metadata addresses
66
- refused; redirects re-validated). LLM-extracted call insights and market
87
+ against SSRF: every DNS answer must be globally routable, validated answers are
88
+ pinned into socket creation, mixed/private/reserved answers are refused, every
89
+ redirect is revalidated, sensitive headers are stripped cross-origin, and
90
+ time/body/redirect limits are enforced. LLM-extracted call insights and market
67
91
  classifications are mechanically verified verbatim against the source text
68
92
  before they can drive a writeback, so a prompt-injected transcript or page
69
93
  cannot fabricate a grounded-looking change. CSV/formula-injection in ingested
package/dist/cli/audit.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { readFileSync, writeFileSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
4
  import { auditSnapshot, defaultPolicy } from "../audit.js";
5
- import { loadConfig, mergePolicy, resolveConfiguredRules } from "../config.js";
5
+ import { loadConfig, mergePolicy, resolveConfiguredRules, rulePackageTrustFromCli } from "../config.js";
6
6
  import { getCredential, resolveHubspotConnection } from "../credentials.js";
7
7
  import { patchPlanToMarkdown } from "../format.js";
8
8
  import { appendHealthEntry, computeHealth, healthToMarkdown, readHealthTimeline, saveWorkspaceSnapshot, summarizeHealth, activeWorkspaceProfile } from "../health.js";
@@ -169,8 +169,9 @@ async function registerHubspotWithBroker() {
169
169
  }
170
170
  export async function audit(args) {
171
171
  const threshold = failOnThreshold(args);
172
- const loaded = loadConfig(option(args, "--config") ?? undefined);
173
- const rules = selectedRules(args, await resolveConfiguredRules(loaded));
172
+ const explicitConfig = option(args, "--config") ?? undefined;
173
+ const loaded = loadConfig(explicitConfig);
174
+ const rules = selectedRules(args, await resolveConfiguredRules(loaded, undefined, rulePackageTrustFromCli(args, explicitConfig)));
174
175
  const snapshot = await readSnapshot(args);
175
176
  const policy = mergePolicy(defaultPolicy(), loaded?.config);
176
177
  const today = option(args, "--today");
@@ -326,8 +327,9 @@ function shortDay(at) {
326
327
  * re-fetching (useful for a plan produced earlier or by another machine).
327
328
  */
328
329
  export async function reportCommand(args) {
329
- const loaded = loadConfig(option(args, "--config") ?? undefined);
330
- const configuredRules = await resolveConfiguredRules(loaded);
330
+ const explicitConfig = option(args, "--config") ?? undefined;
331
+ const loaded = loadConfig(explicitConfig);
332
+ const configuredRules = await resolveConfiguredRules(loaded, undefined, rulePackageTrustFromCli(args, explicitConfig));
331
333
  let plan;
332
334
  let snapshot;
333
335
  const planPath = option(args, "--plan");
@@ -375,8 +377,9 @@ export async function reportCommand(args) {
375
377
  }
376
378
  }
377
379
  export async function rulesCommand(args) {
378
- const loaded = loadConfig(option(args, "--config") ?? undefined);
379
- const rules = await resolveConfiguredRules(loaded);
380
+ const explicitConfig = option(args, "--config") ?? undefined;
381
+ const loaded = loadConfig(explicitConfig);
382
+ const rules = await resolveConfiguredRules(loaded, undefined, rulePackageTrustFromCli(args, explicitConfig));
380
383
  if (args.includes("--json")) {
381
384
  console.log(JSON.stringify(rules.map(({ id, title, description, category }) => ({
382
385
  id,
package/dist/cli/auth.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { existsSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
4
  import { DEFAULT_LOOPBACK_PORT, openInBrowser, runHubspotLoopbackLogin, validateHubspotToken } from "../connectors/hubspotAuth.js";
5
- import { pollSalesforceDeviceLogin, startSalesforceDeviceLogin, validateSalesforceToken } from "../connectors/salesforceAuth.js";
5
+ import { pollSalesforceDeviceLogin, startSalesforceDeviceLogin, validateSalesforceOrigin, validateSalesforceToken } from "../connectors/salesforceAuth.js";
6
6
  import { activeProfile, credentialsPath, DEFAULT_PROFILE, deleteCredential, getCredential, storeCredential } from "../credentials.js";
7
7
  import { activeWorkspaceProfile, readHealthTimeline, summarizeHealth } from "../health.js";
8
8
  import { resolveLlmCredential, validateLlmKey } from "../llm.js";
@@ -229,7 +229,10 @@ async function salesforceLogin(args) {
229
229
  await guidedProviderLogin("salesforce", args, "--device requires --client-id (the consumer key of a Connected App with device flow enabled).");
230
230
  return;
231
231
  }
232
- const loginUrl = option(args, "--login-url") ?? undefined;
232
+ const loginUrlInput = option(args, "--login-url") ?? undefined;
233
+ const loginUrl = loginUrlInput
234
+ ? validateSalesforceOrigin(loginUrlInput, "Salesforce --login-url")
235
+ : undefined;
233
236
  const authorization = await startSalesforceDeviceLogin({ clientId, loginUrl });
234
237
  console.error(`\nPairing code: ${authorization.userCode}\n\nConfirm this code at:\n\n ${authorization.verificationUri}\n`);
235
238
  void openInBrowser(authorization.verificationUri);
@@ -254,11 +257,12 @@ async function salesforceLogin(args) {
254
257
  console.log("Tokens refresh silently; no further browser interaction is needed.");
255
258
  return;
256
259
  }
257
- const instanceUrl = option(args, "--instance-url");
258
- if (!instanceUrl) {
260
+ const instanceUrlInput = option(args, "--instance-url");
261
+ if (!instanceUrlInput) {
259
262
  await guidedProviderLogin("salesforce", args, "Salesforce login needs hosted OAuth, --device --client-id <consumer key>, or --instance-url <https://yourorg.my.salesforce.com> with the access token piped on stdin.");
260
263
  return;
261
264
  }
265
+ const instanceUrl = validateSalesforceOrigin(instanceUrlInput, "Salesforce --instance-url");
262
266
  const token = await readSecret("Salesforce access token");
263
267
  if (!token)
264
268
  throw new Error("No access token provided.");
package/dist/cli/fix.js CHANGED
@@ -2,10 +2,11 @@
2
2
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
4
  import { auditSnapshot, defaultPolicy } from "../audit.js";
5
- import { loadConfig, mergePolicy, resolveConfiguredRules } from "../config.js";
5
+ import { loadConfig, mergePolicy, resolveConfiguredRules, rulePackageTrustFromCli } from "../config.js";
6
6
  import { applyPatchPlan } from "../connector.js";
7
7
  import { patchPlanToMarkdown } from "../format.js";
8
8
  import { createFilePlanStore } from "../planStore.js";
9
+ import { verifyApprovalDigests } from "../integrity.js";
9
10
  import { resolveRecord } from "../resolve.js";
10
11
  import { parseAssignmentPolicy } from "../assign.js";
11
12
  import { buildLeadRoutePlan } from "../route.js";
@@ -197,8 +198,9 @@ export async function fixCommand(args) {
197
198
  throw new Error("--min-confidence must be high or low");
198
199
  }
199
200
  const includeCreates = args.includes("--include-creates");
200
- const loaded = loadConfig(option(args, "--config") ?? undefined);
201
- const configured = await resolveConfiguredRules(loaded);
201
+ const explicitConfig = option(args, "--config") ?? undefined;
202
+ const loaded = loadConfig(explicitConfig);
203
+ const configured = await resolveConfiguredRules(loaded, undefined, rulePackageTrustFromCli(args, explicitConfig));
202
204
  const rule = configured.find((candidate) => candidate.id === ruleId);
203
205
  if (!rule) {
204
206
  throw new Error(`Unknown rule: ${ruleId}. Available rules: ${configured.map((r) => r.id).join(", ")}`);
@@ -266,22 +268,33 @@ export async function fixCommand(args) {
266
268
  return;
267
269
  }
268
270
  const connector = await connectorFor(provider, args);
271
+ const claimed = await store.claimApply(plan.id, { provider, source: "fix" });
272
+ const { claimId } = claimed;
273
+ const claimedVerification = verifyApprovalDigests(claimed.stored.plan.operations, claimed.stored.approvedOperationIds, claimed.stored.valueOverrides, claimed.stored.approvalDigests);
274
+ if (!claimedVerification.ok) {
275
+ await store.abortApplyPreflight(plan.id, claimId, "Approval integrity verification failed after the claim and before provider I/O.");
276
+ throw new Error(`Refusing to apply plan ${plan.id}: approval changed while acquiring the apply claim.`);
277
+ }
269
278
  // Live apply board on interactive terminals (stderr; inert otherwise); the
270
279
  // same emitter streams heartbeats to the paired hosted app on long runs.
271
280
  const renderer = createProgressRenderer(APPLY_STAGES);
272
281
  const progress = createProgressEmitter(composeListeners(renderer.listener, progressReporter()));
273
282
  let run;
274
283
  try {
275
- run = await applyPatchPlan(connector, plan, {
276
- approvedOperationIds: approvedIds,
277
- valueOverrides: overrides,
284
+ run = await applyPatchPlan(connector, claimed.stored.plan, {
285
+ approvedOperationIds: claimed.stored.approvedOperationIds,
286
+ valueOverrides: claimed.stored.valueOverrides,
278
287
  progress,
279
288
  });
280
289
  }
290
+ catch (error) {
291
+ await store.markApplyUncertain(plan.id, claimId);
292
+ throw error;
293
+ }
281
294
  finally {
282
295
  renderer.done();
283
296
  }
284
- await store.recordRun(plan.id, run);
297
+ await store.recordRun(plan.id, run, claimId);
285
298
  const counts = { applied: 0, conflict: 0, skipped: 0, failed: 0 };
286
299
  for (const result of run.results)
287
300
  counts[result.status] = (counts[result.status] ?? 0) + 1;
package/dist/cli/help.js CHANGED
@@ -179,6 +179,7 @@ Usage:
179
179
  fullstackgtm plans list [--status <s>] | show <id> | reject <id>
180
180
  fullstackgtm plans approve <id> --operations <ids|all> [--value <opId>=<v>]
181
181
  fullstackgtm plans approve <id> --values-from <suggestions.json> [--min-confidence high|low] [--include-creates]
182
+ fullstackgtm plans recover <id> --acknowledge-uncertain-writes
182
183
  fullstackgtm apply --plan-id <id> --provider <name>
183
184
  fullstackgtm apply --plan-id <id> --channel outbox (render approved openers; transmits nothing)
184
185
  fullstackgtm apply --plan <path> --provider <name> --approve <ids|all> [options]
@@ -226,6 +227,8 @@ Audit options:
226
227
  --config <path> Config file (default: ./fullstackgtm.config.json if present)
227
228
  { "policy": {...}, "rules": {"enabled":[],"disabled":[]},
228
229
  "rulePackages": ["./team-rules.mjs"] }
230
+ --allow-plugins Execute rulePackages from an explicit --config path after review
231
+ --no-plugins Ignore rulePackages; use declarative config and built-in rules only
229
232
  --rules <ids> Comma-separated rule ids to run (default: all; see \`rules\`)
230
233
  --json Print the JSON patch plan instead of markdown
231
234
  --out <path> Also write the JSON patch plan to a file
@@ -498,6 +501,7 @@ export const HELP = {
498
501
  "fullstackgtm plans list [--status <s>] | show <id> | reject <id>",
499
502
  "fullstackgtm plans approve <id> --operations <ids|all> [--value <opId>=<v>]",
500
503
  "fullstackgtm plans approve <id> --values-from <suggestions.json> [--min-confidence high|low]",
504
+ "fullstackgtm plans recover <id> --acknowledge-uncertain-writes",
501
505
  ],
502
506
  detail: "Approval is explicit and per-operation; placeholders need a concrete --value or a suggestion to be approvable.",
503
507
  seeAlso: ["audit", "suggest", "apply"],
@@ -585,11 +589,11 @@ export const HELP = {
585
589
  };
586
590
  // Verbs that print their own richer multi-subcommand help; runCli routes their
587
591
  // `--help` to themselves, so commandHelp() only renders these via `help <verb>`.
588
- export const BESPOKE_HELP = ["init", "call", "market", "tam", "enrich", "bulk-update", "schedule", "signals", "icp", "draft"];
592
+ export const BESPOKE_HELP = ["init", "call", "market", "tam", "enrich", "schedule", "signals", "icp", "draft"];
589
593
  export const GLOBAL_FLAGS = ["--help", "--full"];
590
594
  export const GLOBAL_SHORT_FLAGS = ["-h"];
591
595
  export const SOURCE_FLAGS = ["--provider", "--token-env", "--input", "--demo", "--sample", "--seed", "--today"];
592
- export const AUDIT_FLAGS = ["--config", "--rules", "--stale-days", "--fail-on", "--save", "--dry-run", "--json", "--out", "--full"];
596
+ export const AUDIT_FLAGS = ["--config", "--allow-plugins", "--no-plugins", "--rules", "--stale-days", "--fail-on", "--save", "--dry-run", "--json", "--out", "--full"];
593
597
  // Complete per-command flag registry used by runCli's fail-closed flag
594
598
  // validation. Keep this next to HELP so focused help, machine capabilities,
595
599
  // and the parser safety gate have one command inventory to reconcile against.
@@ -605,13 +609,13 @@ export const COMMAND_FLAGS = {
605
609
  snapshot: [...SOURCE_FLAGS, "--since", "--out", "--archive"],
606
610
  audit: [...SOURCE_FLAGS, ...AUDIT_FLAGS],
607
611
  report: [...SOURCE_FLAGS, ...AUDIT_FLAGS, "--plan", "--client", "--title", "--prepared-by", "--format", "--max-examples"],
608
- diff: ["--before", "--after", "--config", "--stale-days", "--today", "--json", "--fail-on-new-findings"],
609
- rules: ["--config", "--json"],
612
+ diff: ["--before", "--after", "--config", "--allow-plugins", "--no-plugins", "--stale-days", "--today", "--json", "--fail-on-new-findings"],
613
+ rules: ["--config", "--allow-plugins", "--no-plugins", "--json"],
610
614
  resolve: [...SOURCE_FLAGS, "--name", "--domain", "--email", "--account-id", "--json"],
611
615
  hierarchy: [...SOURCE_FLAGS, "--json", "--out"],
612
616
  relationships: [...SOURCE_FLAGS, "--account-id", "--domain", "--json", "--out"],
613
617
  route: [...SOURCE_FLAGS, "--match", "--no-inherit-owner", "--reassign-owned", "--policy", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
614
- fix: [...SOURCE_FLAGS, "--config", "--rule", "--provider", "--min-confidence", "--include-creates", "--yes", "--confirm", "--dry-run"],
618
+ fix: [...SOURCE_FLAGS, "--config", "--allow-plugins", "--no-plugins", "--rule", "--provider", "--min-confidence", "--include-creates", "--yes", "--confirm", "--dry-run"],
615
619
  "bulk-update": [...SOURCE_FLAGS, "--where", "--set", "--guard", "--require", "--create-task", "--archive", "--force-archive-duplicates", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
616
620
  dedupe: [...SOURCE_FLAGS, "--key", "--keep", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
617
621
  reassign: [...SOURCE_FLAGS, "--from", "--assign-unowned", "--to", "--objects", "--where", "--except-deal-stage", "--include-closed-deals", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
@@ -619,7 +623,7 @@ export const COMMAND_FLAGS = {
619
623
  enrich: [...SOURCE_FLAGS, "--config", "--source", "--icp", "--input", "--provider", "--stale-days", "--assign-owner", "--objects", "--max", "--staged-run", "--run-label", "--label", "--runs", "--save", "--dry-run", "--json", "--out"],
620
624
  call: [...SOURCE_FLAGS, "--transcript", "--title", "--source", "--model", "--deterministic", "--heuristics", "--llm", "--json", "--ndjson", "--out", "--call", "--call-type", "--rubric", "--list", "--list-rubrics", "--attendees", "--domain", "--deal", "--save", "--dry-run"],
621
625
  suggest: [...SOURCE_FLAGS, "--plan-id", "--plan", "--json", "--out"],
622
- plans: ["--status", "--operations", "--value", "--values-from", "--min-confidence", "--include-creates", "--json"],
626
+ plans: ["--status", "--operations", "--value", "--values-from", "--min-confidence", "--include-creates", "--acknowledge-uncertain-writes", "--json"],
623
627
  apply: ["--plan", "--plan-id", "--provider", "--channel", "--token-env", "--approve", "--value", "--json", "--config"],
624
628
  "audit-log": ["--in", "--out", "--json"],
625
629
  merge: ["--input", "--out", "--json"],