thinkwork-cli 0.13.2 → 1.0.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 (231) hide show
  1. package/README.md +3 -14
  2. package/dist/{api-client-JYZXYQJR.js → api-client-4P4JZOBX.js} +1 -1
  3. package/dist/{chunk-Z2F6RJFR.js → chunk-4OMDWYZQ.js} +4 -5
  4. package/dist/{chunk-ILZKG5WV.js → chunk-AD2NIWB5.js} +434 -69
  5. package/dist/chunk-MWQ7RO76.js +137 -0
  6. package/dist/{chunk-DVPBI5HM.js → chunk-OLUQ4U2Z.js} +26 -26
  7. package/dist/{chunk-QOERT44A.js → chunk-YQCYD6NE.js} +57 -44
  8. package/dist/cli.js +4400 -16431
  9. package/dist/commands/enterprise/templates/deploy-repo/scripts/apply-release.mjs +0 -1
  10. package/dist/commands/enterprise/templates/deploy-repo/scripts/smoke.mjs +0 -1
  11. package/dist/commands/enterprise/templates/deploy-repo/terraform/main.tf +0 -14
  12. package/dist/{dist-es-CRV46D5B.js → dist-es-2OTNTL4N.js} +4 -4
  13. package/dist/{dist-es-ENBUC3JP.js → dist-es-2XVAH4BQ.js} +2 -2
  14. package/dist/{dist-es-DCY4IC3Y.js → dist-es-3DPDUJAH.js} +2 -2
  15. package/dist/{dist-es-G7PTG75M.js → dist-es-BNZTBBVC.js} +1 -1
  16. package/dist/{dist-es-GKDBE7E6.js → dist-es-CD2RBDAC.js} +1 -1
  17. package/dist/{dist-es-R7FGL2IB.js → dist-es-I4FW57PS.js} +1 -1
  18. package/dist/{dist-es-USORBXZ5.js → dist-es-KVFWJNDY.js} +4 -4
  19. package/dist/{dist-es-HY3535OI.js → dist-es-PS73DWNL.js} +2 -2
  20. package/dist/{dist-es-LDE437AM.js → dist-es-S3NZQFGT.js} +1 -1
  21. package/dist/{dist-es-G54LBMRN.js → dist-es-SGCA4P32.js} +1 -1
  22. package/dist/{dist-es-Q57SZXZ7.js → dist-es-WBM6J4RA.js} +2 -2
  23. package/dist/{dist-es-P26FWFLT.js → dist-es-ZGVADYIF.js} +8 -8
  24. package/dist/drizzle/0227_analyst_reader_role.sql +17 -26
  25. package/dist/drizzle/0230_analyst_rls.sql +28 -45
  26. package/dist/drizzle/0261_native_auth_control_plane.sql +7 -3
  27. package/dist/drizzle/0270_tool_execution_ledger.sql +115 -0
  28. package/dist/drizzle/0271_ontology_twin_declarations.sql +47 -0
  29. package/dist/drizzle/0272_identity_graph_projection_cursor.sql +23 -0
  30. package/dist/drizzle/0273_twin_materialization_suggestions.sql +29 -0
  31. package/dist/drizzle/0274_tenant_mcp_twin_keys.sql +59 -0
  32. package/dist/drizzle/0275_identity_bulk_rebuild_fence.sql +30 -0
  33. package/dist/drizzle/0276_tool_execution_ledger_allow_cascade_delete.sql +18 -0
  34. package/dist/drizzle/0277_knowledge_base_sources.sql +107 -0
  35. package/dist/drizzle/0278_kb_page_transcription.sql +55 -0
  36. package/dist/drizzle/0279_mcp_servers_off_plugin_provenance.sql +93 -0
  37. package/dist/drizzle/0280_work_item_external_refs_drop_plane_provider.sql +46 -0
  38. package/dist/drizzle/0281_tenant_mcp_twin_keys_expiry_suffix.sql +9 -0
  39. package/dist/drizzle/0282_tenant_mcp_twin_keys_grants.sql +24 -0
  40. package/dist/drizzle/0283_agents_agentcore_runtime_dispatch.sql +13 -0
  41. package/dist/drizzle/0284_user_brain_claims.sql +69 -0
  42. package/dist/drizzle/0285_tenant_mcp_twin_keys_trusted_subsystem.sql +30 -0
  43. package/dist/drizzle/0286_brain_analytics_key.sql +26 -0
  44. package/dist/drizzle/0287_brain_subject.sql +25 -0
  45. package/dist/drizzle/0288_multi_lane_identities.sql +25 -0
  46. package/dist/{loadSso-DJVB7BXE.js → loadSso-ETCXTR4I.js} +2 -2
  47. package/dist/{loadSso-JAMNA3WY.js → loadSso-UBD7KUHE.js} +2 -2
  48. package/dist/plugins/README.md +2 -2
  49. package/dist/plugins/catalog/package.json +0 -3
  50. package/dist/plugins/catalog/src/__tests__/build-catalog.test.ts +6 -3
  51. package/dist/plugins/catalog/src/__tests__/catalog.test.ts +21 -21
  52. package/dist/plugins/catalog/src/__tests__/contracts.test.ts +102 -25
  53. package/dist/plugins/catalog/src/__tests__/plugin-package.test.ts +7 -6
  54. package/dist/plugins/catalog/src/__tests__/plugin-registry.test.ts +0 -15
  55. package/dist/plugins/catalog/src/contracts.ts +15 -0
  56. package/dist/plugins/catalog/src/registry/generated-first-party.ts +0 -18
  57. package/dist/plugins/n8n/test/integrated-app-contract.test.ts +1 -1
  58. package/dist/plugins/twenty/package.json +2 -1
  59. package/dist/plugins/twenty/scripts/LAMBDA_DEPLOY.md +114 -0
  60. package/dist/plugins/twenty/scripts/build-lambda.sh +29 -0
  61. package/dist/plugins/twenty/scripts/configure-product-views.ts +195 -0
  62. package/dist/plugins/twenty/scripts/lambda-handler.ts +49 -0
  63. package/dist/plugins/twenty/scripts/lib/__tests__/load-attachments.test.ts +38 -0
  64. package/dist/plugins/twenty/scripts/lib/__tests__/load-records.test.ts +145 -2
  65. package/dist/plugins/twenty/scripts/lib/__tests__/mappers.test.ts +397 -151
  66. package/dist/plugins/twenty/scripts/lib/__tests__/members-ensure.test.ts +28 -129
  67. package/dist/plugins/twenty/scripts/lib/__tests__/provision-members-db.test.ts +10 -0
  68. package/dist/plugins/twenty/scripts/lib/__tests__/schema-ensure.test.ts +13 -8
  69. package/dist/plugins/twenty/scripts/lib/lastmile-reader.ts +222 -2
  70. package/dist/plugins/twenty/scripts/lib/load-attachments.ts +29 -0
  71. package/dist/plugins/twenty/scripts/lib/load-records.ts +160 -43
  72. package/dist/plugins/twenty/scripts/lib/mappers.ts +368 -152
  73. package/dist/plugins/twenty/scripts/lib/members-ensure.ts +43 -268
  74. package/dist/plugins/twenty/scripts/lib/provision-members-db.ts +57 -0
  75. package/dist/plugins/twenty/scripts/lib/schema-ensure.ts +136 -31
  76. package/dist/plugins/twenty/scripts/migrate-lastmile.ts +254 -152
  77. package/dist/plugins/twenty/scripts/provision-twenty-members.ts +44 -1
  78. package/dist/plugins/twenty/scripts/purge-lastmile-import.ts +178 -0
  79. package/dist/plugins/twenty/scripts/restructure-products.ts +312 -0
  80. package/dist/plugins/twenty/scripts/retag-stage-values.ts +362 -0
  81. package/dist/plugins/twenty/test/manifest.test.ts +2 -4
  82. package/dist/scripts/post-deploy.sh +79 -1
  83. package/dist/{signin-UGDWTGS4.js → signin-5QZNRSOO.js} +2 -2
  84. package/dist/{sso-oidc-Q4VXANXY.js → sso-oidc-XK673P54.js} +2 -2
  85. package/dist/{sts-CWLSE3GH.js → sts-OXI2UALF.js} +6 -134
  86. package/dist/{sts-DNZLNVOV.js → sts-SQW4NBK3.js} +6 -134
  87. package/dist/terraform/examples/greenfield/main.tf +233 -175
  88. package/dist/terraform/examples/greenfield/terraform.tfvars.example +12 -8
  89. package/dist/terraform/modules/app/agentcore-admin/main.tf +47 -0
  90. package/dist/terraform/modules/app/agentcore-code-interpreter/README.md +39 -47
  91. package/dist/terraform/modules/app/agentcore-code-interpreter/main.tf +13 -66
  92. package/dist/terraform/modules/app/agentcore-identity/main.tf +106 -26
  93. package/dist/terraform/modules/app/agentcore-identity/outputs.tf +6 -6
  94. package/dist/terraform/modules/app/agentcore-identity/scripts/bootstrap_twenty_oauth_client.sh +216 -99
  95. package/dist/terraform/modules/app/agentcore-identity/scripts/delete_identity.sh +6 -9
  96. package/dist/terraform/modules/app/agentcore-identity/scripts/delete_workload_identity.sh +19 -0
  97. package/dist/terraform/modules/app/agentcore-identity/scripts/ensure_workload_identity.sh +29 -0
  98. package/dist/terraform/modules/app/agentcore-identity/scripts/read_identity.sh +22 -7
  99. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_identity.sh +17 -46
  100. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_provider.mjs +65 -3
  101. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/after.tf +15 -0
  102. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/before.tf +7 -0
  103. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/identity-after.tf +8 -0
  104. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/identity-before.tf +9 -0
  105. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/thinkwork-after.tf +3 -0
  106. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/thinkwork-before.tf +3 -0
  107. package/dist/terraform/modules/app/agentcore-identity/variables.tf +6 -0
  108. package/dist/terraform/modules/app/agentcore-memory/README.md +46 -9
  109. package/dist/terraform/modules/app/agentcore-memory/main.tf +27 -10
  110. package/dist/terraform/modules/app/agentcore-memory/scripts/create_or_find_memory.sh +156 -24
  111. package/dist/terraform/modules/app/agentcore-pi/README.md +1 -3
  112. package/dist/terraform/modules/app/agentcore-pi/main.tf +42 -52
  113. package/dist/terraform/modules/app/agentcore-pi/outputs.tf +3 -3
  114. package/dist/terraform/modules/app/agentcore-pi/scripts/reconcile_pi_runtime.mjs +175 -0
  115. package/dist/terraform/modules/app/agentcore-pi/variables.tf +35 -28
  116. package/dist/terraform/modules/app/agentcore-platform/main.tf +68 -0
  117. package/dist/terraform/modules/app/agentcore-runtime/main.tf +4 -20
  118. package/dist/terraform/modules/app/capability-broker/main.tf +2 -2
  119. package/dist/terraform/modules/app/capability-broker/variables.tf +11 -0
  120. package/dist/terraform/modules/app/deployment-control-plane/README.md +264 -0
  121. package/dist/terraform/modules/app/deployment-control-plane/buildspec.yml +4 -1
  122. package/dist/terraform/modules/app/deployment-control-plane/main.tf +94 -0
  123. package/dist/terraform/modules/app/deployment-control-plane/outputs.tf +5 -0
  124. package/dist/terraform/modules/app/deployment-control-plane/runner.py +2641 -124
  125. package/dist/terraform/modules/app/deployment-control-plane/test_runner_bundle.py +2034 -154
  126. package/dist/terraform/modules/app/deployment-control-plane/variables.tf +33 -0
  127. package/dist/terraform/modules/app/lambda-api/auth-state.tf +72 -0
  128. package/dist/terraform/modules/app/lambda-api/chat-latency-observability.tf +466 -0
  129. package/dist/terraform/modules/app/lambda-api/dispatch-dlq.tf +55 -0
  130. package/dist/terraform/modules/app/lambda-api/handlers.tf +411 -723
  131. package/dist/terraform/modules/app/lambda-api/iam-grouped.tf +262 -198
  132. package/dist/terraform/modules/app/lambda-api/main.tf +51 -13
  133. package/dist/terraform/modules/app/lambda-api/mcp-oauth.tf +4 -181
  134. package/dist/terraform/modules/app/lambda-api/memory-alarms.tf +3 -28
  135. package/dist/terraform/modules/app/lambda-api/oauth-secrets.tf +1 -2
  136. package/dist/terraform/modules/app/lambda-api/outputs.tf +17 -45
  137. package/dist/terraform/modules/app/lambda-api/turn-assertion.tf +45 -0
  138. package/dist/terraform/modules/app/lambda-api/variables.tf +219 -126
  139. package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/README.md +1 -1
  140. package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/main.tf +18 -18
  141. package/dist/terraform/modules/app/sandbox-log-scrubber/README.md +11 -10
  142. package/dist/terraform/modules/app/sandbox-log-scrubber/main.tf +5 -5
  143. package/dist/terraform/modules/app/static-site/main.tf +1 -1
  144. package/dist/terraform/modules/app/www-dns/main.tf +9 -27
  145. package/dist/terraform/modules/app/www-dns/variables.tf +4 -10
  146. package/dist/terraform/modules/data/aurora-postgres/README.md +34 -0
  147. package/dist/terraform/modules/data/aurora-postgres/alarms.tf +82 -0
  148. package/dist/terraform/modules/data/aurora-postgres/main.tf +27 -57
  149. package/dist/terraform/modules/data/aurora-postgres/outputs.tf +0 -9
  150. package/dist/terraform/modules/data/aurora-postgres/variables.tf +41 -0
  151. package/dist/terraform/modules/data/s3-buckets/main.tf +5 -6
  152. package/dist/terraform/modules/foundation/cognito/main.tf +81 -8
  153. package/dist/terraform/modules/foundation/cognito/outputs.tf +5 -0
  154. package/dist/terraform/modules/foundation/cognito/variables.tf +24 -12
  155. package/dist/terraform/modules/thinkwork/README.md +5 -1
  156. package/dist/terraform/modules/thinkwork/main.tf +376 -374
  157. package/dist/terraform/modules/thinkwork/outputs.tf +12 -158
  158. package/dist/terraform/modules/thinkwork/variables.tf +246 -89
  159. package/dist/workspace-defaults/files/INSTRUCTIONS.md +9 -11
  160. package/dist/workspace-defaults/files/MEMORY_GUIDE.md +38 -35
  161. package/dist/workspace-defaults/files/ROUTER.md +1 -1
  162. package/dist/workspace-defaults/files/TOOLS.md +9 -39
  163. package/dist/workspace-defaults/files/agents/analyst/INSTRUCTIONS.md +6 -3
  164. package/dist/workspace-defaults/files/catalog-skills/brain-brief-builder/SKILL.md +165 -0
  165. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/SKILL.md +75 -0
  166. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/mcp-tooling.md +40 -0
  167. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/validation-and-handoff.md +40 -0
  168. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/workflow-authoring.md +46 -0
  169. package/dist/workspace-defaults/files/skills/artifact-builder/references/crm-dashboard.md +1 -1
  170. package/dist/workspace-defaults/files/skills/document-composer/SKILL.md +22 -3
  171. package/package.json +1 -1
  172. package/dist/drizzle/0261_managed_harness_multiplayer.sql +0 -313
  173. package/dist/drizzle/0262_parallel_harness_threads.sql +0 -9
  174. package/dist/drizzle/0264_harness_tool_execution_ledger.sql +0 -113
  175. package/dist/plugins/company-data/README.md +0 -49
  176. package/dist/plugins/company-data/package.json +0 -21
  177. package/dist/plugins/company-data/src/index.ts +0 -30
  178. package/dist/plugins/company-data/src/manifest.ts +0 -26
  179. package/dist/plugins/company-data/test/manifest.test.ts +0 -116
  180. package/dist/plugins/company-data/tsconfig.json +0 -8
  181. package/dist/plugins/company-etl/README.md +0 -46
  182. package/dist/plugins/company-etl/package.json +0 -21
  183. package/dist/plugins/company-etl/src/index.ts +0 -29
  184. package/dist/plugins/company-etl/src/manifest.ts +0 -26
  185. package/dist/plugins/company-etl/test/manifest.test.ts +0 -108
  186. package/dist/plugins/company-etl/tsconfig.json +0 -8
  187. package/dist/plugins/lastmile/README.md +0 -50
  188. package/dist/plugins/lastmile/package.json +0 -23
  189. package/dist/plugins/lastmile/smoke/lastmile-plugin-smoke.mjs +0 -661
  190. package/dist/plugins/lastmile/src/api/tasks-adapter.ts +0 -572
  191. package/dist/plugins/lastmile/src/discovery.fixture.ts +0 -64
  192. package/dist/plugins/lastmile/src/index.ts +0 -46
  193. package/dist/plugins/lastmile/src/manifest.ts +0 -139
  194. package/dist/plugins/lastmile/test/api/tasks-adapter.test.ts +0 -266
  195. package/dist/plugins/lastmile/test/discovery.test.ts +0 -88
  196. package/dist/plugins/lastmile/tsconfig.json +0 -8
  197. package/dist/plugins/n8n/smoke/n8n-managed-app-smoke.mjs +0 -409
  198. package/dist/plugins/twenty/smoke/twenty-managed-app-smoke.mjs +0 -359
  199. package/dist/terraform/modules/app/agentcore-code-interpreter/Dockerfile.sandbox-base +0 -61
  200. package/dist/terraform/modules/app/agentcore-code-interpreter/sandbox/sitecustomize.py +0 -227
  201. package/dist/terraform/modules/app/agentcore-code-interpreter/sandbox/test_sitecustomize.py +0 -277
  202. package/dist/terraform/modules/app/agentcore-code-interpreter/scripts/build_and_push_sandbox_base.sh +0 -70
  203. package/dist/terraform/modules/app/agentcore-gateway/main.tf +0 -305
  204. package/dist/terraform/modules/app/agentcore-gateway/outputs.tf +0 -44
  205. package/dist/terraform/modules/app/agentcore-gateway/scripts/delete_gateway.sh +0 -93
  206. package/dist/terraform/modules/app/agentcore-gateway/scripts/read_gateway.sh +0 -33
  207. package/dist/terraform/modules/app/agentcore-gateway/scripts/reconcile_gateway.sh +0 -543
  208. package/dist/terraform/modules/app/agentcore-gateway/variables.tf +0 -70
  209. package/dist/terraform/modules/app/agentcore-harness/main.tf +0 -330
  210. package/dist/terraform/modules/app/agentcore-harness/outputs.tf +0 -72
  211. package/dist/terraform/modules/app/agentcore-harness/scripts/delete_harness.sh +0 -5
  212. package/dist/terraform/modules/app/agentcore-harness/scripts/harness-lifecycle.mjs +0 -430
  213. package/dist/terraform/modules/app/agentcore-harness/scripts/harness-readback.mjs +0 -180
  214. package/dist/terraform/modules/app/agentcore-harness/scripts/harness-tool-contract.mjs +0 -193
  215. package/dist/terraform/modules/app/agentcore-harness/scripts/prune_harness.sh +0 -5
  216. package/dist/terraform/modules/app/agentcore-harness/scripts/read_harness.sh +0 -5
  217. package/dist/terraform/modules/app/agentcore-harness/scripts/reconcile_harness.sh +0 -5
  218. package/dist/terraform/modules/app/agentcore-harness/variables.tf +0 -96
  219. package/dist/terraform/modules/app/hindsight-memory/README.md +0 -89
  220. package/dist/terraform/modules/app/hindsight-memory/main.tf +0 -411
  221. package/dist/terraform/modules/app/lambda-api/agentcore-harness-alarms.tf +0 -88
  222. package/dist/terraform/modules/app/lambda-api/agentcore-identity-alarms.tf +0 -91
  223. package/dist/terraform/modules/data/bedrock-knowledge-base/main.tf +0 -102
  224. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/outputs.tf +0 -0
  225. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/scripts/sync-database.py +0 -0
  226. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/scripts/test_sync_database.py +0 -0
  227. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/variables.tf +0 -0
  228. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/README.md +0 -0
  229. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/main.tf +0 -0
  230. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/outputs.tf +0 -0
  231. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/variables.tf +0 -0
@@ -0,0 +1,362 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * One-off: strip the `LM_` prefix from opportunity stage VALUES, and remove the
4
+ * stage options no record uses.
5
+ *
6
+ * The first import wrote values like `LM_10_PROSPECT`. Labels were always clean
7
+ * ("10-Prospect"), but the raw value surfaces in the API, in filters, and in
8
+ * generated reports. It also left 8 options behind from an abandoned model.
9
+ *
10
+ * Order matters. Deleting an option while records still reference its value
11
+ * strands those records on a value the field no longer offers, so this runs in
12
+ * three phases:
13
+ *
14
+ * 1. ADD — write options = existing ∪ clean values. Both old and new exist.
15
+ * 2. RETAG — move every record from its LM_ value to the clean one.
16
+ * 3. PRUNE — write options = final set, dropping LM_* and the 8 orphans.
17
+ *
18
+ * A crash between phases is safe: phase 1 is idempotent, phase 2 re-runs over
19
+ * whatever is left, and phase 3 refuses to drop a value that still has records.
20
+ *
21
+ * CUSTOMER is never dropped — the ThinkWork workflow triggers on it.
22
+ *
23
+ * Usage:
24
+ * npx tsx scripts/retag-stage-values.ts # dry-run: show the plan
25
+ * npx tsx scripts/retag-stage-values.ts --apply
26
+ */
27
+
28
+ import process from "node:process";
29
+
30
+ import {
31
+ LEGACY_STAGE_VALUE_MAP,
32
+ MIGRATION_STAGE_OPTIONS,
33
+ OBSOLETE_STAGE_VALUES,
34
+ } from "./lib/mappers";
35
+ import { chunk, TwentyClient, normalizeBaseUrl } from "./lib/twenty-client";
36
+
37
+ interface StageOption {
38
+ id?: string;
39
+ label: string;
40
+ value: string;
41
+ color: string;
42
+ position: number;
43
+ }
44
+
45
+ const UPDATE_BATCH = 60;
46
+
47
+ function requireEnv(name: string): string {
48
+ const value = process.env[name];
49
+ if (!value) throw new Error(`Missing required environment variable: ${name}`);
50
+ return value;
51
+ }
52
+
53
+ function log(message: string): void {
54
+ process.stderr.write(`${message}\n`);
55
+ }
56
+
57
+ async function readStageField(
58
+ client: TwentyClient,
59
+ ): Promise<{ id: string; options: StageOption[] }> {
60
+ const data = await client.requestWithRetry<{
61
+ objects: {
62
+ edges: Array<{
63
+ node: {
64
+ nameSingular: string;
65
+ fields: {
66
+ edges: Array<{
67
+ node: { id: string; name: string; options: StageOption[] | null };
68
+ }>;
69
+ };
70
+ };
71
+ }>;
72
+ };
73
+ }>(
74
+ "/metadata",
75
+ `query RetagStageField {
76
+ objects(paging: { first: 1000 }) {
77
+ edges { node { nameSingular fields(paging: { first: 1000 }) { edges { node { id name options } } } } }
78
+ }
79
+ }`,
80
+ );
81
+ const opportunity = data.objects.edges.find(
82
+ (edge) => edge.node.nameSingular === "opportunity",
83
+ );
84
+ const stage = opportunity?.node.fields.edges.find(
85
+ (edge) => edge.node.name === "stage",
86
+ )?.node;
87
+ if (!stage?.options) throw new Error("opportunity.stage field not found.");
88
+ return { id: stage.id, options: stage.options };
89
+ }
90
+
91
+ async function writeOptions(
92
+ client: TwentyClient,
93
+ fieldId: string,
94
+ options: StageOption[],
95
+ ): Promise<void> {
96
+ await client.requestOnce(
97
+ "/metadata",
98
+ `mutation RetagWriteOptions($input: UpdateOneFieldMetadataInput!) {
99
+ updateOneField(input: $input) { id }
100
+ }`,
101
+ { input: { id: fieldId, update: { options } } },
102
+ );
103
+ }
104
+
105
+ /**
106
+ * Confirm a freshly-added option value is actually live before any record is
107
+ * moved onto it. Twenty accepts the metadata write immediately, but the
108
+ * underlying Postgres enum lags — and a record written to a not-yet-live value
109
+ * is silently coerced to the field's default rather than rejected. That cost
110
+ * 1,510 opportunities their stage on 2026-07-10; they were rebuilt from
111
+ * LastMile, but the write must never be attempted blind again.
112
+ */
113
+ async function waitForOptionsLive(
114
+ client: TwentyClient,
115
+ values: readonly string[],
116
+ attempts = 10,
117
+ ): Promise<void> {
118
+ for (const value of values) {
119
+ let live = false;
120
+ for (let attempt = 0; attempt < attempts && !live; attempt += 1) {
121
+ try {
122
+ await client.requestWithRetry(
123
+ "/graphql",
124
+ `query RetagProbe($filter: OpportunityFilterInput) {
125
+ opportunities(filter: $filter, first: 1) { edges { node { id } } }
126
+ }`,
127
+ { filter: { stage: { eq: value } } },
128
+ );
129
+ live = true;
130
+ } catch {
131
+ await new Promise((resolve) => setTimeout(resolve, 2_000));
132
+ }
133
+ }
134
+ if (!live) {
135
+ throw new Error(
136
+ `Stage option "${value}" never became queryable; refusing to retag records onto it.`,
137
+ );
138
+ }
139
+ }
140
+ }
141
+
142
+ /** Count records per stage value so PRUNE can refuse to strand any. */
143
+ async function countByStage(
144
+ client: TwentyClient,
145
+ ): Promise<Map<string, number>> {
146
+ const counts = new Map<string, number>();
147
+ let after: string | null = null;
148
+ for (;;) {
149
+ const data: {
150
+ opportunities: {
151
+ edges: Array<{ node: { id: string; stage: string | null } }>;
152
+ pageInfo: { hasNextPage: boolean; endCursor: string | null };
153
+ };
154
+ } = await client.requestWithRetry(
155
+ "/graphql",
156
+ `query RetagCount($after: String) {
157
+ opportunities(first: 100, after: $after) {
158
+ edges { node { id stage } cursor }
159
+ pageInfo { hasNextPage endCursor }
160
+ }
161
+ }`,
162
+ after ? { after } : {},
163
+ );
164
+ for (const { node } of data.opportunities.edges) {
165
+ const key = node.stage ?? "NULL";
166
+ counts.set(key, (counts.get(key) ?? 0) + 1);
167
+ }
168
+ if (!data.opportunities.pageInfo.hasNextPage) break;
169
+ after = data.opportunities.pageInfo.endCursor;
170
+ }
171
+ return counts;
172
+ }
173
+
174
+ async function idsForStage(
175
+ client: TwentyClient,
176
+ stage: string,
177
+ ): Promise<string[]> {
178
+ const ids: string[] = [];
179
+ let after: string | null = null;
180
+ for (;;) {
181
+ const data: {
182
+ opportunities: {
183
+ edges: Array<{ node: { id: string } }>;
184
+ pageInfo: { hasNextPage: boolean; endCursor: string | null };
185
+ };
186
+ } = await client.requestWithRetry(
187
+ "/graphql",
188
+ `query RetagIds($filter: OpportunityFilterInput, $after: String) {
189
+ opportunities(filter: $filter, first: 100, after: $after) {
190
+ edges { node { id } cursor }
191
+ pageInfo { hasNextPage endCursor }
192
+ }
193
+ }`,
194
+ { filter: { stage: { eq: stage } }, ...(after ? { after } : {}) },
195
+ );
196
+ ids.push(...data.opportunities.edges.map((edge) => edge.node.id));
197
+ if (!data.opportunities.pageInfo.hasNextPage) break;
198
+ after = data.opportunities.pageInfo.endCursor;
199
+ }
200
+ return ids;
201
+ }
202
+
203
+ async function main(): Promise<void> {
204
+ const apply = process.argv.slice(2).includes("--apply");
205
+ const client = new TwentyClient({
206
+ baseUrl: normalizeBaseUrl(requireEnv("TWENTY_PUBLIC_URL")),
207
+ authToken: requireEnv("TWENTY_API_KEY"),
208
+ });
209
+
210
+ const { id: fieldId, options } = await readStageField(client);
211
+ const before = await countByStage(client);
212
+ const report: Record<string, unknown> = {
213
+ mode: apply ? "apply" : "dry-run",
214
+ stageCountsBefore: Object.fromEntries(before),
215
+ };
216
+
217
+ const cleanValues = new Set(MIGRATION_STAGE_OPTIONS.map((o) => o.value));
218
+ const retagged = Object.entries(LEGACY_STAGE_VALUE_MAP).filter(
219
+ ([from]) => (before.get(from) ?? 0) > 0,
220
+ );
221
+ report.willRetag = Object.fromEntries(
222
+ retagged.map(([from, to]) => [from, `${to} (${before.get(from)} records)`]),
223
+ );
224
+
225
+ // --- Phase 1: ADD -------------------------------------------------------
226
+ const existingValues = new Set(options.map((option) => option.value));
227
+ const additions = MIGRATION_STAGE_OPTIONS.filter(
228
+ (option) => !existingValues.has(option.value),
229
+ );
230
+ const migrationByValue = new Map(
231
+ MIGRATION_STAGE_OPTIONS.map((option) => [option.value, option]),
232
+ );
233
+ // A reused option (NEW, PROSPECT, WON, LOST, FORMULATE_OFFER) must take
234
+ // LastMile's label and colour, or the picker shows "Won" where the source
235
+ // says "60-Won".
236
+ const relabelled: string[] = [];
237
+ const phase1: StageOption[] = [
238
+ ...options.map((option, index) => {
239
+ const replacement = migrationByValue.get(option.value);
240
+ if (!replacement) return { ...option, position: index };
241
+ if (option.label !== replacement.label) relabelled.push(option.value);
242
+ return {
243
+ ...option,
244
+ label: replacement.label,
245
+ color: replacement.color,
246
+ position: index,
247
+ };
248
+ }),
249
+ ...additions.map((option, index) => ({
250
+ label: option.label,
251
+ value: option.value,
252
+ color: option.color,
253
+ position: options.length + index,
254
+ })),
255
+ ];
256
+ report.phase1Add = additions.map((option) => option.value);
257
+ report.phase1Relabel = relabelled;
258
+
259
+ if (apply && (additions.length > 0 || relabelled.length > 0)) {
260
+ log(
261
+ `phase 1: adding ${additions.length} options, relabelling ${relabelled.length}...`,
262
+ );
263
+ await writeOptions(client, fieldId, phase1);
264
+ // The enum lags the metadata write; moving records onto a value that is not
265
+ // yet live silently coerces them to the default.
266
+ log("phase 1: waiting for new options to go live...");
267
+ await waitForOptionsLive(
268
+ client,
269
+ additions.map((option) => option.value),
270
+ );
271
+ }
272
+
273
+ // --- Phase 2: RETAG -----------------------------------------------------
274
+ const moved: Record<string, number> = {};
275
+ if (apply) {
276
+ for (const [from, to] of retagged) {
277
+ const ids = await idsForStage(client, from);
278
+ log(`phase 2: ${from} -> ${to} (${ids.length} records)`);
279
+ for (const batch of chunk(ids, UPDATE_BATCH)) {
280
+ await client.requestOnce(
281
+ "/graphql",
282
+ `mutation RetagMove($filter: OpportunityFilterInput!, $data: OpportunityUpdateInput!) {
283
+ updateOpportunities(filter: $filter, data: $data) { id }
284
+ }`,
285
+ { filter: { id: { in: batch } }, data: { stage: to } },
286
+ );
287
+ }
288
+ moved[from] = ids.length;
289
+ }
290
+ }
291
+ report.phase2Moved = apply ? moved : "(dry-run)";
292
+
293
+ // --- Phase 3: PRUNE -----------------------------------------------------
294
+ const after = apply ? await countByStage(client) : before;
295
+
296
+ // Every record must have landed on the value we aimed it at. Verify BEFORE
297
+ // pruning, because pruning is the destructive step and a silent coercion
298
+ // would otherwise be locked in.
299
+ if (apply) {
300
+ const expected = new Map<string, number>();
301
+ for (const [from, to] of Object.entries(LEGACY_STAGE_VALUE_MAP)) {
302
+ const n = before.get(from) ?? 0;
303
+ if (n > 0) expected.set(to, (expected.get(to) ?? 0) + n);
304
+ }
305
+ const drift = [...expected.entries()].filter(
306
+ ([value, count]) => (after.get(value) ?? 0) !== count,
307
+ );
308
+ if (drift.length > 0) {
309
+ throw new Error(
310
+ "Retag did not land as planned — refusing to prune. " +
311
+ drift
312
+ .map(
313
+ ([value, want]) =>
314
+ `${value}: expected ${want}, found ${after.get(value) ?? 0}`,
315
+ )
316
+ .join("; ") +
317
+ ". Re-run migrate-lastmile.ts --apply to rebuild stages from LastMile.",
318
+ );
319
+ }
320
+ }
321
+ const dropCandidates = [
322
+ ...Object.keys(LEGACY_STAGE_VALUE_MAP),
323
+ ...OBSOLETE_STAGE_VALUES,
324
+ ];
325
+ const stillUsed = dropCandidates.filter(
326
+ (value) => (after.get(value) ?? 0) > 0,
327
+ );
328
+ if (stillUsed.length > 0 && apply) {
329
+ throw new Error(
330
+ `Refusing to prune stage options still referenced by records: ${stillUsed.join(", ")}`,
331
+ );
332
+ }
333
+ const keep = phase1.filter(
334
+ (option) =>
335
+ !dropCandidates.includes(option.value) || cleanValues.has(option.value),
336
+ );
337
+ report.phase3Prune = dropCandidates.filter(
338
+ (value) => !cleanValues.has(value) && existingValues.has(value),
339
+ );
340
+ report.finalOptions = keep.map((option) => option.value);
341
+
342
+ if (apply) {
343
+ log(
344
+ `phase 3: pruning ${(report.phase3Prune as string[]).length} options...`,
345
+ );
346
+ await writeOptions(
347
+ client,
348
+ fieldId,
349
+ keep.map((option, index) => ({ ...option, position: index })),
350
+ );
351
+ report.stageCountsAfter = Object.fromEntries(await countByStage(client));
352
+ }
353
+
354
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
355
+ }
356
+
357
+ main().catch((error) => {
358
+ process.stderr.write(
359
+ `${error instanceof Error ? error.message : String(error)}\n`,
360
+ );
361
+ process.exit(1);
362
+ });
@@ -359,10 +359,8 @@ describe("twenty plugin manifest", () => {
359
359
  });
360
360
 
361
361
  it("enables Twenty local logic functions for the first-party workflow action", () => {
362
- const terraformModule = readRepo("plugins/twenty/terraform/twenty/main.tf");
363
- const terraformReadme = readRepo(
364
- "plugins/twenty/terraform/twenty/README.md",
365
- );
362
+ const terraformModule = readRepo("terraform/modules/app/twenty/main.tf");
363
+ const terraformReadme = readRepo("terraform/modules/app/twenty/README.md");
366
364
 
367
365
  expect(terraformModule).toContain(
368
366
  '{ name = "LOGIC_FUNCTION_TYPE", value = "LOCAL" }',
@@ -30,6 +30,7 @@ STRICT=0
30
30
  JSON=0
31
31
  REGION="${AWS_REGION:-us-east-1}"
32
32
  MIN_SOURCE_SHA=""
33
+ EXPECTED_DIGEST=""
33
34
  RUNTIME="pi"
34
35
  AGENTCORE_CONTROL_TIMEOUT_SECONDS="${AGENTCORE_CONTROL_TIMEOUT_SECONDS:-20}"
35
36
 
@@ -41,6 +42,7 @@ while [[ $# -gt 0 ]]; do
41
42
  --region) REGION="${2:?}"; shift 2 ;;
42
43
  --runtime) RUNTIME="${2:?}"; shift 2 ;;
43
44
  --min-source-sha) MIN_SOURCE_SHA="${2:?}"; shift 2 ;;
45
+ --expected-digest) EXPECTED_DIGEST="${2:?}"; shift 2 ;;
44
46
  --help|-h)
45
47
  sed -n '3,30p' "$0"
46
48
  exit 0
@@ -57,6 +59,11 @@ if [[ -z "$STAGE" ]]; then
57
59
  exit 2
58
60
  fi
59
61
 
62
+ if [[ -n "$EXPECTED_DIGEST" && ! "$EXPECTED_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]; then
63
+ echo "ERROR: --expected-digest must be sha256:<64 hex chars> (got '$EXPECTED_DIGEST')" >&2
64
+ exit 2
65
+ fi
66
+
60
67
  case "$RUNTIME" in
61
68
  pi) ;;
62
69
  strands)
@@ -91,12 +98,61 @@ log() {
91
98
 
92
99
  image_sha_from_uri() {
93
100
  local image_uri="$1"
101
+ # Digest-pinned URIs (repo@sha256:...) carry no tag; without this guard the
102
+ # first 40 hex chars of the digest would masquerade as a source commit sha.
103
+ if [[ "$image_uri" == *"@sha256:"* ]]; then
104
+ return 0
105
+ fi
94
106
  local tag="${image_uri##*:}"
95
107
  if [[ "$tag" =~ ([0-9a-f]{40}) ]]; then
96
108
  echo "${BASH_REMATCH[1]}"
97
109
  fi
98
110
  }
99
111
 
112
+ digest_from_uri() {
113
+ local image_uri="$1"
114
+ if [[ "$image_uri" =~ @(sha256:[0-9a-f]{64})$ ]]; then
115
+ echo "${BASH_REMATCH[1]}"
116
+ fi
117
+ }
118
+
119
+ source_sha_from_digest() {
120
+ # A digest-pinned image URI carries no tag, but the image still has its
121
+ # build tags in ECR — recover the 40-hex source sha from them so the
122
+ # ancestry check works for digest-pinned runtimes. Two builds of the same
123
+ # source produce different digests, so digest EQUALITY against a
124
+ # source-sha-derived tag is the wrong invariant (2026-08-04 deploy
125
+ # 30872795807); tag-recovered ancestry is the right one.
126
+ local image_uri="$1" digest="$2"
127
+ local repo_path="${image_uri%%@*}"
128
+ local repo_name="${repo_path##*/}"
129
+ local tags tag errfile
130
+ # Keep stdout (the recovered sha) separate from any describe-images error so
131
+ # a failed recovery is diagnosable on stderr instead of collapsing to a bare
132
+ # `sha=unknown` — that opacity is what made THINK-1119 hard to trace. Callers
133
+ # that know the exact pinned digest should pass --expected-digest, which
134
+ # skips this ECR-tag round-trip entirely.
135
+ errfile=$(mktemp)
136
+ if ! tags=$(aws ecr describe-images \
137
+ --repository-name "$repo_name" \
138
+ --region "$REGION" \
139
+ --image-ids "imageDigest=$digest" \
140
+ --query 'imageDetails[0].imageTags' \
141
+ --output json 2>"$errfile"); then
142
+ echo "WARN: ecr describe-images for ${repo_name}@${digest} failed: $(tr '\n' ' ' <"$errfile")" >&2
143
+ rm -f "$errfile"
144
+ return 0
145
+ fi
146
+ rm -f "$errfile"
147
+ while IFS= read -r tag; do
148
+ if [[ "$tag" =~ ([0-9a-f]{40}) ]]; then
149
+ echo "${BASH_REMATCH[1]}"
150
+ return 0
151
+ fi
152
+ done < <(echo "$tags" | jq -r '.[]? // empty')
153
+ echo "WARN: no 40-hex source-sha tag on ${repo_name}@${digest} (tags=$(echo "$tags" | jq -c '. // []' 2>/dev/null)); pass --expected-digest to assert provenance directly" >&2
154
+ }
155
+
100
156
  image_contains_source_sha() {
101
157
  local image_sha="$1"
102
158
  local source_sha="$2"
@@ -200,6 +256,7 @@ while read -r rt_id rt_name rt_status; do
200
256
  rt_version=$(echo "$rt_detail" | jq -r '.agentRuntimeVersion // "null"')
201
257
  rt_image=$(echo "$rt_detail" | jq -r '.agentRuntimeArtifact.containerConfiguration.containerUri // ""')
202
258
  rt_image_sha=$(image_sha_from_uri "$rt_image")
259
+ rt_digest=$(digest_from_uri "$rt_image")
203
260
 
204
261
  # DEFAULT endpoint is the one Terraform manages for us
205
262
  eps=$(agentcore_control list-agent-runtime-endpoints \
@@ -248,7 +305,28 @@ while read -r rt_id rt_name rt_status; do
248
305
  is_clean=0
249
306
  reasons+=("endpoint liveVersion=$ep_live != runtime version=$rt_version")
250
307
  fi
251
- if [[ -n "$MIN_SOURCE_SHA" && ( -z "$ACTIVE_RUNTIME_ID" || "$rt_id" == "$ACTIVE_RUNTIME_ID" ) ]] && ! image_contains_source_sha "$rt_image_sha" "$MIN_SOURCE_SHA"; then
308
+ # Digest pinning (THINK-584 U5): --expected-digest is for callers that
309
+ # know the EXACT digest the runtime was just pinned to (the control-plane
310
+ # runner). When it's set and the runtime serves a digest-pinned image,
311
+ # they must match. A legacy TAG-form runtime image is not a digest
312
+ # mismatch — it falls through to the ancestry check, so the first
313
+ # post-merge deploy (runtime not yet re-pinned) doesn't false-fail.
314
+ digest_matched=0
315
+ if [[ -n "$EXPECTED_DIGEST" && -n "$rt_digest" && ( -z "$ACTIVE_RUNTIME_ID" || "$rt_id" == "$ACTIVE_RUNTIME_ID" ) ]]; then
316
+ if [[ "$rt_digest" != "$EXPECTED_DIGEST" ]]; then
317
+ is_clean=0
318
+ reasons+=("runtime image digest=${rt_digest} != expected digest=$EXPECTED_DIGEST")
319
+ else
320
+ digest_matched=1
321
+ fi
322
+ fi
323
+ # Ancestry check: a digest-pinned image recovers its source sha from the
324
+ # image's ECR tags. A matched expected digest is stronger provenance and
325
+ # skips ancestry.
326
+ if [[ -n "$rt_digest" && -z "$rt_image_sha" && "$digest_matched" -eq 0 ]]; then
327
+ rt_image_sha=$(source_sha_from_digest "$rt_image" "$rt_digest")
328
+ fi
329
+ if [[ "$digest_matched" -eq 0 && -n "$MIN_SOURCE_SHA" && ( -z "$ACTIVE_RUNTIME_ID" || "$rt_id" == "$ACTIVE_RUNTIME_ID" ) ]] && ! image_contains_source_sha "$rt_image_sha" "$MIN_SOURCE_SHA"; then
252
330
  is_clean=0
253
331
  reasons+=("runtime image sha=${rt_image_sha:-unknown} does not include required source sha=$MIN_SOURCE_SHA")
254
332
  fi
@@ -7,7 +7,7 @@ import {
7
7
  AwsSdkSigV4Signer,
8
8
  NODE_AUTH_SCHEME_PREFERENCE_OPTIONS,
9
9
  resolveAwsSdkSigV4Config
10
- } from "./chunk-DVPBI5HM.js";
10
+ } from "./chunk-OLUQ4U2Z.js";
11
11
  import "./chunk-EI5ZX7CN.js";
12
12
  import {
13
13
  Sha256Node
@@ -53,7 +53,7 @@ import {
53
53
  resolveHostHeaderConfig,
54
54
  resolveRetryConfig,
55
55
  resolveUserAgentConfig
56
- } from "./chunk-QOERT44A.js";
56
+ } from "./chunk-YQCYD6NE.js";
57
57
  import {
58
58
  NODE_REGION_CONFIG_FILE_OPTIONS,
59
59
  NODE_REGION_CONFIG_OPTIONS,
@@ -7,7 +7,7 @@ import {
7
7
  AwsSdkSigV4Signer,
8
8
  NODE_AUTH_SCHEME_PREFERENCE_OPTIONS,
9
9
  resolveAwsSdkSigV4Config
10
- } from "./chunk-DVPBI5HM.js";
10
+ } from "./chunk-OLUQ4U2Z.js";
11
11
  import "./chunk-EI5ZX7CN.js";
12
12
  import {
13
13
  Sha256Node
@@ -53,7 +53,7 @@ import {
53
53
  resolveHostHeaderConfig,
54
54
  resolveRetryConfig,
55
55
  resolveUserAgentConfig
56
- } from "./chunk-QOERT44A.js";
56
+ } from "./chunk-YQCYD6NE.js";
57
57
  import {
58
58
  NODE_REGION_CONFIG_FILE_OPTIONS,
59
59
  NODE_REGION_CONFIG_OPTIONS,