@poa-box/core 0.1.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 (220) hide show
  1. package/README.md +201 -0
  2. package/dist/abis/DirectDemocracyVotingNew.d.ts +749 -0
  3. package/dist/abis/DirectDemocracyVotingNew.js +961 -0
  4. package/dist/abis/ERC20.d.ts +90 -0
  5. package/dist/abis/ERC20.js +125 -0
  6. package/dist/abis/EducationHubNew.d.ts +590 -0
  7. package/dist/abis/EducationHubNew.js +762 -0
  8. package/dist/abis/EligibilityModuleNew.d.ts +1615 -0
  9. package/dist/abis/EligibilityModuleNew.js +2082 -0
  10. package/dist/abis/Executor.d.ts +619 -0
  11. package/dist/abis/Executor.js +796 -0
  12. package/dist/abis/HybridVotingNew.d.ts +898 -0
  13. package/dist/abis/HybridVotingNew.js +1149 -0
  14. package/dist/abis/ImplementationRegistry.d.ts +270 -0
  15. package/dist/abis/ImplementationRegistry.js +355 -0
  16. package/dist/abis/OrgDeployerNew.d.ts +1275 -0
  17. package/dist/abis/OrgDeployerNew.js +1630 -0
  18. package/dist/abis/OrgRegistry.d.ts +670 -0
  19. package/dist/abis/OrgRegistry.js +868 -0
  20. package/dist/abis/ParticipationToken.d.ts +1086 -0
  21. package/dist/abis/ParticipationToken.js +1415 -0
  22. package/dist/abis/PasskeyAccount.d.ts +788 -0
  23. package/dist/abis/PasskeyAccount.js +1019 -0
  24. package/dist/abis/PasskeyAccountFactory.d.ts +344 -0
  25. package/dist/abis/PasskeyAccountFactory.js +449 -0
  26. package/dist/abis/PaymasterHub.d.ts +1603 -0
  27. package/dist/abis/PaymasterHub.js +2048 -0
  28. package/dist/abis/PaymentManager.d.ts +520 -0
  29. package/dist/abis/PaymentManager.js +671 -0
  30. package/dist/abis/PoaManager.d.ts +344 -0
  31. package/dist/abis/PoaManager.js +449 -0
  32. package/dist/abis/QuickJoinNew.d.ts +855 -0
  33. package/dist/abis/QuickJoinNew.js +1098 -0
  34. package/dist/abis/TaskManagerNew.d.ts +1236 -0
  35. package/dist/abis/TaskManagerNew.js +1573 -0
  36. package/dist/abis/ToggleModule.d.ts +193 -0
  37. package/dist/abis/ToggleModule.js +255 -0
  38. package/dist/abis/UniversalAccountRegistry.d.ts +577 -0
  39. package/dist/abis/UniversalAccountRegistry.js +750 -0
  40. package/dist/abis/ZkEmailInvites.d.ts +823 -0
  41. package/dist/abis/ZkEmailInvites.js +1060 -0
  42. package/dist/abis/external/AaveGovernanceV2.d.ts +128 -0
  43. package/dist/abis/external/AaveGovernanceV2.js +177 -0
  44. package/dist/abis/external/AaveGovernanceV3.d.ts +160 -0
  45. package/dist/abis/external/AaveGovernanceV3.js +220 -0
  46. package/dist/abis/external/AragonVoting.d.ts +107 -0
  47. package/dist/abis/external/AragonVoting.js +150 -0
  48. package/dist/abis/external/CurveGaugeController.d.ts +95 -0
  49. package/dist/abis/external/CurveGaugeController.js +130 -0
  50. package/dist/abis/external/CurveVotingEscrow.d.ts +86 -0
  51. package/dist/abis/external/CurveVotingEscrow.js +116 -0
  52. package/dist/abis/external/GovernorAlpha.d.ts +172 -0
  53. package/dist/abis/external/GovernorAlpha.js +228 -0
  54. package/dist/abis/external/MakerDAOChief.d.ts +92 -0
  55. package/dist/abis/external/MakerDAOChief.js +129 -0
  56. package/dist/abis/external/OZGovernor.d.ts +227 -0
  57. package/dist/abis/external/OZGovernor.js +317 -0
  58. package/dist/abis/external/SolidlyVotingEscrow.d.ts +196 -0
  59. package/dist/abis/external/SolidlyVotingEscrow.js +260 -0
  60. package/dist/abis/index.d.ts +62 -0
  61. package/dist/abis/index.js +98 -0
  62. package/dist/chains.d.ts +150 -0
  63. package/dist/chains.js +320 -0
  64. package/dist/context.d.ts +48 -0
  65. package/dist/context.js +38 -0
  66. package/dist/contracts.d.ts +24 -0
  67. package/dist/contracts.js +41 -0
  68. package/dist/encoding.d.ts +58 -0
  69. package/dist/encoding.js +243 -0
  70. package/dist/env.d.ts +10 -0
  71. package/dist/env.js +4 -0
  72. package/dist/error-catalog.d.ts +41 -0
  73. package/dist/error-catalog.js +941 -0
  74. package/dist/errors.d.ts +27 -0
  75. package/dist/errors.js +60 -0
  76. package/dist/execute/ethers.d.ts +93 -0
  77. package/dist/execute/ethers.js +323 -0
  78. package/dist/execute/index.d.ts +7 -0
  79. package/dist/execute/index.js +23 -0
  80. package/dist/execute/sponsored.d.ts +68 -0
  81. package/dist/execute/sponsored.js +277 -0
  82. package/dist/exit-codes.d.ts +18 -0
  83. package/dist/exit-codes.js +21 -0
  84. package/dist/format.d.ts +21 -0
  85. package/dist/format.js +89 -0
  86. package/dist/graph/client.d.ts +298 -0
  87. package/dist/graph/client.js +702 -0
  88. package/dist/graph/documents/activity.d.ts +9 -0
  89. package/dist/graph/documents/activity.js +164 -0
  90. package/dist/graph/documents/beacons.d.ts +172 -0
  91. package/dist/graph/documents/beacons.js +345 -0
  92. package/dist/graph/documents/index.d.ts +21 -0
  93. package/dist/graph/documents/index.js +57 -0
  94. package/dist/graph/documents/infrastructure.d.ts +30 -0
  95. package/dist/graph/documents/infrastructure.js +34 -0
  96. package/dist/graph/documents/org.d.ts +15 -0
  97. package/dist/graph/documents/org.js +249 -0
  98. package/dist/graph/documents/paymaster.d.ts +95 -0
  99. package/dist/graph/documents/paymaster.js +101 -0
  100. package/dist/graph/documents/role.d.ts +6 -0
  101. package/dist/graph/documents/role.js +41 -0
  102. package/dist/graph/documents/roles.d.ts +50 -0
  103. package/dist/graph/documents/roles.js +112 -0
  104. package/dist/graph/documents/task.d.ts +84 -0
  105. package/dist/graph/documents/task.js +236 -0
  106. package/dist/graph/documents/token.d.ts +13 -0
  107. package/dist/graph/documents/token.js +126 -0
  108. package/dist/graph/documents/treasury.d.ts +39 -0
  109. package/dist/graph/documents/treasury.js +136 -0
  110. package/dist/graph/documents/user.d.ts +120 -0
  111. package/dist/graph/documents/user.js +302 -0
  112. package/dist/graph/documents/voting-classes.d.ts +69 -0
  113. package/dist/graph/documents/voting-classes.js +149 -0
  114. package/dist/graph/documents/voting.d.ts +28 -0
  115. package/dist/graph/documents/voting.js +163 -0
  116. package/dist/graph/documents/vouch.d.ts +79 -0
  117. package/dist/graph/documents/vouch.js +171 -0
  118. package/dist/graph/documents/zkemail.d.ts +63 -0
  119. package/dist/graph/documents/zkemail.js +188 -0
  120. package/dist/graph/index.d.ts +2 -0
  121. package/dist/graph/index.js +41 -0
  122. package/dist/index.d.ts +49 -0
  123. package/dist/index.js +92 -0
  124. package/dist/ipfs.d.ts +39 -0
  125. package/dist/ipfs.js +214 -0
  126. package/dist/label-aliases.d.ts +31 -0
  127. package/dist/label-aliases.js +70 -0
  128. package/dist/metadata/education.d.ts +30 -0
  129. package/dist/metadata/education.js +31 -0
  130. package/dist/metadata/index.d.ts +12 -0
  131. package/dist/metadata/index.js +48 -0
  132. package/dist/metadata/org.d.ts +92 -0
  133. package/dist/metadata/org.js +93 -0
  134. package/dist/metadata/proposal.d.ts +44 -0
  135. package/dist/metadata/proposal.js +45 -0
  136. package/dist/metadata/role.d.ts +38 -0
  137. package/dist/metadata/role.js +35 -0
  138. package/dist/metadata/task.d.ts +108 -0
  139. package/dist/metadata/task.js +81 -0
  140. package/dist/metadata/token.d.ts +32 -0
  141. package/dist/metadata/token.js +36 -0
  142. package/dist/metadata/user.d.ts +63 -0
  143. package/dist/metadata/user.js +70 -0
  144. package/dist/multicall.d.ts +32 -0
  145. package/dist/multicall.js +96 -0
  146. package/dist/payout.d.ts +59 -0
  147. package/dist/payout.js +96 -0
  148. package/dist/perms.d.ts +47 -0
  149. package/dist/perms.js +120 -0
  150. package/dist/preflight.d.ts +72 -0
  151. package/dist/preflight.js +269 -0
  152. package/dist/reads/education.d.ts +79 -0
  153. package/dist/reads/education.js +122 -0
  154. package/dist/reads/eligibility.d.ts +377 -0
  155. package/dist/reads/eligibility.js +687 -0
  156. package/dist/reads/index.d.ts +16 -0
  157. package/dist/reads/index.js +55 -0
  158. package/dist/reads/org.d.ts +258 -0
  159. package/dist/reads/org.js +220 -0
  160. package/dist/reads/paymaster.d.ts +147 -0
  161. package/dist/reads/paymaster.js +235 -0
  162. package/dist/reads/project.d.ts +33 -0
  163. package/dist/reads/project.js +62 -0
  164. package/dist/reads/resolve.d.ts +34 -0
  165. package/dist/reads/resolve.js +64 -0
  166. package/dist/reads/task.d.ts +286 -0
  167. package/dist/reads/task.js +280 -0
  168. package/dist/reads/token.d.ts +94 -0
  169. package/dist/reads/token.js +75 -0
  170. package/dist/reads/treasury.d.ts +222 -0
  171. package/dist/reads/treasury.js +241 -0
  172. package/dist/reads/user.d.ts +229 -0
  173. package/dist/reads/user.js +155 -0
  174. package/dist/reads/vote.d.ts +296 -0
  175. package/dist/reads/vote.js +392 -0
  176. package/dist/reads/zkemail.d.ts +117 -0
  177. package/dist/reads/zkemail.js +217 -0
  178. package/dist/similarity.d.ts +36 -0
  179. package/dist/similarity.js +67 -0
  180. package/dist/sponsorship-config.d.ts +47 -0
  181. package/dist/sponsorship-config.js +41 -0
  182. package/dist/stats.d.ts +11 -0
  183. package/dist/stats.js +27 -0
  184. package/dist/task-lens.d.ts +113 -0
  185. package/dist/task-lens.js +210 -0
  186. package/dist/tx/education.d.ts +169 -0
  187. package/dist/tx/education.js +344 -0
  188. package/dist/tx/eligibility.d.ts +511 -0
  189. package/dist/tx/eligibility.js +818 -0
  190. package/dist/tx/governance.d.ts +85 -0
  191. package/dist/tx/governance.js +87 -0
  192. package/dist/tx/index.d.ts +17 -0
  193. package/dist/tx/index.js +56 -0
  194. package/dist/tx/intent.d.ts +67 -0
  195. package/dist/tx/intent.js +48 -0
  196. package/dist/tx/org.d.ts +321 -0
  197. package/dist/tx/org.js +544 -0
  198. package/dist/tx/paymaster.d.ts +140 -0
  199. package/dist/tx/paymaster.js +251 -0
  200. package/dist/tx/project.d.ts +146 -0
  201. package/dist/tx/project.js +217 -0
  202. package/dist/tx/task.d.ts +480 -0
  203. package/dist/tx/task.js +1149 -0
  204. package/dist/tx/token.d.ts +96 -0
  205. package/dist/tx/token.js +165 -0
  206. package/dist/tx/treasury.d.ts +474 -0
  207. package/dist/tx/treasury.js +911 -0
  208. package/dist/tx/user.d.ts +177 -0
  209. package/dist/tx/user.js +292 -0
  210. package/dist/tx/vote.d.ts +332 -0
  211. package/dist/tx/vote.js +762 -0
  212. package/dist/tx/zkemail.d.ts +90 -0
  213. package/dist/tx/zkemail.js +210 -0
  214. package/dist/validation.d.ts +8 -0
  215. package/dist/validation.js +39 -0
  216. package/dist/version.d.ts +142 -0
  217. package/dist/version.js +257 -0
  218. package/dist/zkemail.d.ts +161 -0
  219. package/dist/zkemail.js +299 -0
  220. package/package.json +130 -0
@@ -0,0 +1,1149 @@
1
+ "use strict";
2
+ /**
3
+ * Task-domain transaction builders (TaskManagerNew).
4
+ *
5
+ * Level 1 — pure builders (sync, zero I/O). One per contract write; they take
6
+ * resolved addresses + prepared values and return a TxIntent. ALL argument
7
+ * encoding lives here, byte-for-byte as the CLI encodes it: stringToBytes
8
+ * titles, ipfsCidToBytes32 hashes, parseUnits amounts (18d PT payouts, token
9
+ * registry decimals for bounties), and the v6/legacy createTask arity switch
10
+ * via TaskManagerFeatures + LEGACY_TM_FRAGMENTS.
11
+ *
12
+ * Level 2 — resolved builders (async, `<action>Intent(ctx, params)`). They
13
+ * resolve the org's TaskManager via reads/resolve, replicate the CLI's
14
+ * payout/pricing derivation and read-merge logic, pin metadata via ../ipfs,
15
+ * then delegate to the Level-1 builder. Duplicate checks, idempotency caches,
16
+ * confirmation prompts and lens pre-flights are CLI UX and are deliberately
17
+ * NOT ported.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.CONFIG_KEY_ROLE_PERM = void 0;
21
+ exports.parseUpdateDeadline = parseUpdateDeadline;
22
+ exports.parseFoldersRoot = parseFoldersRoot;
23
+ exports.parseHatId = parseHatId;
24
+ exports.buildCreateTask = buildCreateTask;
25
+ exports.buildCreateTasksBatch = buildCreateTasksBatch;
26
+ exports.buildClaimTask = buildClaimTask;
27
+ exports.buildUnclaimTask = buildUnclaimTask;
28
+ exports.buildSubmitTask = buildSubmitTask;
29
+ exports.buildCompleteTask = buildCompleteTask;
30
+ exports.buildRejectTask = buildRejectTask;
31
+ exports.buildCancelTask = buildCancelTask;
32
+ exports.buildAssignTask = buildAssignTask;
33
+ exports.buildApplyForTask = buildApplyForTask;
34
+ exports.buildApproveApplication = buildApproveApplication;
35
+ exports.buildUpdateTask = buildUpdateTask;
36
+ exports.buildUpdateTaskMetadata = buildUpdateTaskMetadata;
37
+ exports.buildSetProjectRolePerm = buildSetProjectRolePerm;
38
+ exports.buildSetGlobalRolePermCall = buildSetGlobalRolePermCall;
39
+ exports.buildProposeGlobalRolePerm = buildProposeGlobalRolePerm;
40
+ exports.buildSetFolders = buildSetFolders;
41
+ exports.createTaskIntent = createTaskIntent;
42
+ exports.createTasksBatchIntents = createTasksBatchIntents;
43
+ exports.claimTaskIntent = claimTaskIntent;
44
+ exports.unclaimTaskIntent = unclaimTaskIntent;
45
+ exports.submitTaskIntent = submitTaskIntent;
46
+ exports.completeTaskIntent = completeTaskIntent;
47
+ exports.rejectTaskIntent = rejectTaskIntent;
48
+ exports.cancelTaskIntent = cancelTaskIntent;
49
+ exports.assignTaskIntent = assignTaskIntent;
50
+ exports.applyForTaskIntent = applyForTaskIntent;
51
+ exports.approveApplicationIntent = approveApplicationIntent;
52
+ exports.updateTaskIntent = updateTaskIntent;
53
+ exports.editTaskMetadataIntent = editTaskMetadataIntent;
54
+ exports.setProjectRolePermIntent = setProjectRolePermIntent;
55
+ exports.proposeGlobalRolePermIntent = proposeGlobalRolePermIntent;
56
+ exports.setFoldersIntent = setFoldersIntent;
57
+ const ethers_1 = require("ethers");
58
+ const governance_1 = require("./governance");
59
+ const contracts_1 = require("../contracts");
60
+ const encoding_1 = require("../encoding");
61
+ const version_1 = require("../version");
62
+ const chains_1 = require("../chains");
63
+ const payout_1 = require("../payout");
64
+ const perms_1 = require("../perms");
65
+ const ipfs_1 = require("../ipfs");
66
+ const errors_1 = require("../errors");
67
+ const exit_codes_1 = require("../exit-codes");
68
+ const validation_1 = require("../validation");
69
+ const resolve_1 = require("../reads/resolve");
70
+ const org_1 = require("../graph/documents/org");
71
+ const task_1 = require("../graph/documents/task");
72
+ const task_2 = require("../reads/task");
73
+ const task_3 = require("../metadata/task");
74
+ const proposal_1 = require("../metadata/proposal");
75
+ const task_lens_1 = require("../task-lens");
76
+ // ─────────────────────────── shared encoding helpers ───────────────────────────
77
+ /** TaskManager.ConfigKey.ROLE_PERM — verified enum index (src/commands/task/perms.ts:49). */
78
+ exports.CONFIG_KEY_ROLE_PERM = 2;
79
+ /**
80
+ * Bounty payout in raw token units, exactly as the CLI computes it
81
+ * (task/create.ts, task/create-batch.ts): parsed only when an amount > 0 was
82
+ * given AND the token is not the zero address; otherwise the literal 0.
83
+ */
84
+ function computeBountyPayoutWei(bountyToken, bountyAmount) {
85
+ if (bountyAmount && Number(bountyAmount) > 0 && bountyToken !== ethers_1.ethers.constants.AddressZero) {
86
+ return ethers_1.ethers.utils.parseUnits(bountyAmount.toString(), (0, chains_1.getTokenDecimals)(bountyToken));
87
+ }
88
+ return 0;
89
+ }
90
+ /** Accepts the CLI's string forms ('7d', ISO, unix) or a pre-resolved number. */
91
+ function parseDeadlineInput(value) {
92
+ return (0, encoding_1.parseDeadline)(String(value));
93
+ }
94
+ /** Accepts the CLI's string forms ('48h', '3600') or a pre-resolved number. */
95
+ function parseWindowInput(value) {
96
+ return (0, encoding_1.parseDurationSeconds)(String(value));
97
+ }
98
+ /**
99
+ * parseDeadline for the update path: the contract deliberately accepts a
100
+ * PAST absolute deadline here (admin lever — immediately opens a CLAIMED
101
+ * task to takeover), so downgrade parseDeadline's past-check to a flag.
102
+ * Only absolute forms (unix seconds / ISO date) can resolve to the past —
103
+ * relative forms ("7d") are always future — so re-parsing with now=0 never
104
+ * changes relative math. Port of src/commands/task/update.ts parseUpdateDeadline.
105
+ */
106
+ function parseUpdateDeadline(input) {
107
+ try {
108
+ return { value: (0, encoding_1.parseDeadline)(input), isPast: false };
109
+ }
110
+ catch (err) {
111
+ if (err instanceof errors_1.CliError && /is in the past/.test(err.message)) {
112
+ return { value: (0, encoding_1.parseDeadline)(input, 0), isPast: true };
113
+ }
114
+ throw err;
115
+ }
116
+ }
117
+ /**
118
+ * Parse a folders-root input: CIDv0 (Qm…), 0x-prefixed bytes32, or "clear".
119
+ * Deliberately stricter than ipfsCidToBytes32 (which keccaks arbitrary
120
+ * strings — a silent footgun for a root pointer).
121
+ * Port of src/commands/task/folders.ts parseFoldersRoot.
122
+ */
123
+ function parseFoldersRoot(input) {
124
+ const trimmed = (input || '').trim();
125
+ if (trimmed.toLowerCase() === 'clear')
126
+ return ethers_1.ethers.constants.HashZero;
127
+ if (trimmed.startsWith('0x')) {
128
+ if (trimmed.length === 66 && ethers_1.ethers.utils.isHexString(trimmed, 32))
129
+ return trimmed.toLowerCase();
130
+ throw new errors_1.CliError(`Invalid bytes32 root "${input}".`, exit_codes_1.EXIT.USAGE, 'Pass a 32-byte 0x-prefixed hex value, a Qm… CIDv0, or "clear".');
131
+ }
132
+ if (trimmed.startsWith('Qm')) {
133
+ const converted = (0, encoding_1.ipfsCidToBytes32)(trimmed);
134
+ if (converted === ethers_1.ethers.constants.HashZero) {
135
+ throw new errors_1.CliError(`Could not decode CIDv0 "${input}".`, exit_codes_1.EXIT.USAGE, 'Check the CID — it must be a base58 CIDv0 (Qm…, 46 chars).');
136
+ }
137
+ return converted;
138
+ }
139
+ throw new errors_1.CliError(`Unparseable --new-root "${input}".`, exit_codes_1.EXIT.USAGE, 'Pass a Qm… CIDv0, a 0x-prefixed bytes32, or "clear".');
140
+ }
141
+ /**
142
+ * Parse a hat-id input (decimal or 0x hex) into a BigNumber.
143
+ * Port of src/commands/task/perms.ts parseHatId (same message).
144
+ */
145
+ function parseHatId(input) {
146
+ try {
147
+ return ethers_1.ethers.BigNumber.from(String(input).trim());
148
+ }
149
+ catch {
150
+ throw new errors_1.CliError(`Invalid --hat "${input}".`, exit_codes_1.EXIT.USAGE, 'Pass the hat ID as a decimal or 0x-hex integer (see pop org roles).');
151
+ }
152
+ }
153
+ /**
154
+ * Port of `pop task create` — src/commands/task/create.ts (createTask send).
155
+ * v6 9-arg: [payoutWei, titleBytes, metadataHash, pid, bountyToken,
156
+ * bountyPayoutWei, requiresApplication, absoluteDeadline, completionWindow];
157
+ * legacy 7-arg drops the two deadline words and uses LEGACY_TM_FRAGMENTS.
158
+ */
159
+ function buildCreateTask(a) {
160
+ const payoutWei = ethers_1.ethers.utils.parseUnits(a.payout.toString(), 18);
161
+ const titleBytes = (0, encoding_1.stringToBytes)(a.title);
162
+ const metadataHash = (0, encoding_1.ipfsCidToBytes32)(a.metadataHash);
163
+ const bountyToken = a.bountyToken || ethers_1.ethers.constants.AddressZero;
164
+ const bountyPayoutWei = computeBountyPayoutWei(bountyToken, a.bountyAmount);
165
+ const requiresApp = a.requiresApplication || false;
166
+ const v6 = a.features.deadlines;
167
+ return {
168
+ to: a.taskManagerAddress,
169
+ abi: v6 ? (0, contracts_1.getAbi)('TaskManagerNew') : version_1.LEGACY_TM_FRAGMENTS,
170
+ method: 'createTask',
171
+ args: v6
172
+ ? [payoutWei, titleBytes, metadataHash, a.projectId, bountyToken, bountyPayoutWei, requiresApp, a.absoluteDeadline ?? 0, a.completionWindow ?? 0]
173
+ : [payoutWei, titleBytes, metadataHash, a.projectId, bountyToken, bountyPayoutWei, requiresApp],
174
+ meta: {
175
+ domain: 'task',
176
+ action: 'create',
177
+ orgId: a.orgId,
178
+ summary: { name: a.title, projectId: a.projectId, payout: String(a.payout), signature: v6 ? 'v6' : 'legacy7' },
179
+ },
180
+ };
181
+ }
182
+ /**
183
+ * Port of `pop task create-batch` (v6 path) — src/commands/task/create-batch.ts.
184
+ * ONE all-or-nothing createTasksBatch(pid, CreateTaskInput[]) transaction.
185
+ * CreateTaskInput tuple field order must match the createTasksBatch components
186
+ * in the synced ABI (NOTE: no pid inside): payout, title, metadataHash,
187
+ * bountyToken, bountyPayout, requiresApplication, absoluteDeadline,
188
+ * completionWindow.
189
+ */
190
+ function buildCreateTasksBatch(a) {
191
+ const inputs = a.tasks.map((task) => {
192
+ const bountyToken = task.bountyToken || ethers_1.ethers.constants.AddressZero;
193
+ return [
194
+ ethers_1.ethers.utils.parseUnits(task.payout.toString(), 18),
195
+ (0, encoding_1.stringToBytes)(task.title),
196
+ (0, encoding_1.ipfsCidToBytes32)(task.metadataHash),
197
+ bountyToken,
198
+ computeBountyPayoutWei(bountyToken, task.bountyAmount),
199
+ task.requiresApplication || false,
200
+ task.absoluteDeadline ?? 0,
201
+ task.completionWindow ?? 0,
202
+ ];
203
+ });
204
+ return {
205
+ to: a.taskManagerAddress,
206
+ abi: (0, contracts_1.getAbi)('TaskManagerNew'),
207
+ method: 'createTasksBatch',
208
+ args: [a.projectId, inputs],
209
+ meta: {
210
+ domain: 'task',
211
+ action: 'create-batch',
212
+ orgId: a.orgId,
213
+ summary: { projectId: a.projectId, tasks: a.tasks.length },
214
+ },
215
+ };
216
+ }
217
+ /** Port of `pop task claim` — src/commands/task/claim.ts (claimTask send). */
218
+ function buildClaimTask(a) {
219
+ const id = (0, encoding_1.parseTaskId)(a.taskId);
220
+ return {
221
+ to: a.taskManagerAddress,
222
+ abi: (0, contracts_1.getAbi)('TaskManagerNew'),
223
+ method: 'claimTask',
224
+ args: [id],
225
+ meta: { domain: 'task', action: 'claim', orgId: a.orgId, summary: { taskId: id } },
226
+ };
227
+ }
228
+ /**
229
+ * Port of `pop task unclaim` — src/commands/task/unclaim.ts (unclaimTask send).
230
+ * TaskManager v7 only; the feature gate lives in unclaimTaskIntent (the
231
+ * encoding itself is version-independent).
232
+ */
233
+ function buildUnclaimTask(a) {
234
+ const id = (0, encoding_1.parseTaskId)(a.taskId);
235
+ return {
236
+ to: a.taskManagerAddress,
237
+ abi: (0, contracts_1.getAbi)('TaskManagerNew'),
238
+ method: 'unclaimTask',
239
+ args: [id],
240
+ meta: { domain: 'task', action: 'unclaim', orgId: a.orgId, summary: { taskId: id } },
241
+ };
242
+ }
243
+ /** Port of `pop task submit` — src/commands/task/submit.ts (submitTask send). */
244
+ function buildSubmitTask(a) {
245
+ const id = (0, encoding_1.parseTaskId)(a.taskId);
246
+ return {
247
+ to: a.taskManagerAddress,
248
+ abi: (0, contracts_1.getAbi)('TaskManagerNew'),
249
+ method: 'submitTask',
250
+ args: [id, (0, encoding_1.ipfsCidToBytes32)(a.submissionHash)],
251
+ meta: { domain: 'task', action: 'submit', orgId: a.orgId, summary: { taskId: id } },
252
+ };
253
+ }
254
+ /**
255
+ * Port of `pop task review --action approve` — src/commands/task/review.ts
256
+ * (completeTask send; releases payout + bounty to the claimer).
257
+ */
258
+ function buildCompleteTask(a) {
259
+ const id = (0, encoding_1.parseTaskId)(a.taskId);
260
+ return {
261
+ to: a.taskManagerAddress,
262
+ abi: (0, contracts_1.getAbi)('TaskManagerNew'),
263
+ method: 'completeTask',
264
+ args: [id],
265
+ meta: { domain: 'task', action: 'complete', orgId: a.orgId, summary: { taskId: id } },
266
+ };
267
+ }
268
+ /**
269
+ * Port of `pop task review --action reject` — src/commands/task/review.ts
270
+ * (rejectTask send; no payout, task returns to CLAIMED).
271
+ */
272
+ function buildRejectTask(a) {
273
+ const id = (0, encoding_1.parseTaskId)(a.taskId);
274
+ return {
275
+ to: a.taskManagerAddress,
276
+ abi: (0, contracts_1.getAbi)('TaskManagerNew'),
277
+ method: 'rejectTask',
278
+ args: [id, (0, encoding_1.ipfsCidToBytes32)(a.rejectionHash)],
279
+ meta: { domain: 'task', action: 'reject', orgId: a.orgId, summary: { taskId: id } },
280
+ };
281
+ }
282
+ /** Port of `pop task cancel` — src/commands/task/cancel.ts (cancelTask send). */
283
+ function buildCancelTask(a) {
284
+ const id = (0, encoding_1.parseTaskId)(a.taskId);
285
+ return {
286
+ to: a.taskManagerAddress,
287
+ abi: (0, contracts_1.getAbi)('TaskManagerNew'),
288
+ method: 'cancelTask',
289
+ args: [id],
290
+ meta: { domain: 'task', action: 'cancel', orgId: a.orgId, summary: { taskId: id } },
291
+ };
292
+ }
293
+ /** Port of `pop task assign` — src/commands/task/assign.ts (assignTask send). */
294
+ function buildAssignTask(a) {
295
+ const id = (0, encoding_1.parseTaskId)(a.taskId);
296
+ return {
297
+ to: a.taskManagerAddress,
298
+ abi: (0, contracts_1.getAbi)('TaskManagerNew'),
299
+ method: 'assignTask',
300
+ args: [id, a.assignee],
301
+ meta: { domain: 'task', action: 'assign', orgId: a.orgId, summary: { taskId: id, assignee: a.assignee } },
302
+ };
303
+ }
304
+ /** Port of `pop task apply` — src/commands/task/apply.ts (applyForTask send). */
305
+ function buildApplyForTask(a) {
306
+ const id = (0, encoding_1.parseTaskId)(a.taskId);
307
+ return {
308
+ to: a.taskManagerAddress,
309
+ abi: (0, contracts_1.getAbi)('TaskManagerNew'),
310
+ method: 'applyForTask',
311
+ args: [id, (0, encoding_1.ipfsCidToBytes32)(a.applicationHash)],
312
+ meta: { domain: 'task', action: 'apply', orgId: a.orgId, summary: { taskId: id } },
313
+ };
314
+ }
315
+ /**
316
+ * Port of `pop task approve-app` — src/commands/task/approve-application.ts
317
+ * (approveApplication send).
318
+ */
319
+ function buildApproveApplication(a) {
320
+ const id = (0, encoding_1.parseTaskId)(a.taskId);
321
+ return {
322
+ to: a.taskManagerAddress,
323
+ abi: (0, contracts_1.getAbi)('TaskManagerNew'),
324
+ method: 'approveApplication',
325
+ args: [id, a.applicant],
326
+ meta: { domain: 'task', action: 'approve-app', orgId: a.orgId, summary: { taskId: id, applicant: a.applicant } },
327
+ };
328
+ }
329
+ /**
330
+ * Port of `pop task update` — src/commands/task/update.ts (updateTask send).
331
+ * updateTask is a FULL OVERWRITE on-chain (8-arg v6 signature): every field is
332
+ * the merged FINAL value, in wei — use updateTaskIntent for the read-merge.
333
+ * The CLI refuses the pre-v6 6-field signature entirely (features.deadlines
334
+ * gate), so this builder only ever emits the v6 shape.
335
+ */
336
+ function buildUpdateTask(a) {
337
+ const id = (0, encoding_1.parseTaskId)(a.taskId);
338
+ return {
339
+ to: a.taskManagerAddress,
340
+ abi: (0, contracts_1.getAbi)('TaskManagerNew'),
341
+ method: 'updateTask',
342
+ args: [
343
+ id,
344
+ a.payoutWei,
345
+ (0, encoding_1.stringToBytes)(a.title),
346
+ (0, encoding_1.ipfsCidToBytes32)(a.metadataHash),
347
+ a.bountyToken,
348
+ a.bountyPayoutWei,
349
+ a.absoluteDeadline,
350
+ a.completionWindow,
351
+ ],
352
+ meta: { domain: 'task', action: 'update', orgId: a.orgId, summary: { taskId: id, title: a.title } },
353
+ };
354
+ }
355
+ /**
356
+ * Port of `pop task edit-meta` — src/commands/task/edit-meta.ts
357
+ * (updateTaskMetadata send). v5+; preserves payout/bounty on-chain but fully
358
+ * overwrites the title + metadata pointer pair.
359
+ */
360
+ function buildUpdateTaskMetadata(a) {
361
+ const id = (0, encoding_1.parseTaskId)(a.taskId);
362
+ return {
363
+ to: a.taskManagerAddress,
364
+ abi: (0, contracts_1.getAbi)('TaskManagerNew'),
365
+ method: 'updateTaskMetadata',
366
+ args: [id, (0, encoding_1.stringToBytes)(a.title), (0, encoding_1.ipfsCidToBytes32)(a.metadataHash)],
367
+ meta: { domain: 'task', action: 'edit-meta', orgId: a.orgId, summary: { taskId: id, title: a.title } },
368
+ };
369
+ }
370
+ /**
371
+ * Port of `pop task perms set` — src/commands/task/perms.ts:309
372
+ * (setProjectRolePerm send; creator-hat/executor gate stays on-chain).
373
+ */
374
+ function buildSetProjectRolePerm(a) {
375
+ const hatId = ethers_1.ethers.BigNumber.from(a.hatId);
376
+ return {
377
+ to: a.taskManagerAddress,
378
+ abi: (0, contracts_1.getAbi)('TaskManagerNew'),
379
+ method: 'setProjectRolePerm',
380
+ args: [a.projectId, hatId, a.mask],
381
+ meta: {
382
+ domain: 'task',
383
+ action: 'perms-set',
384
+ orgId: a.orgId,
385
+ summary: { projectId: a.projectId, hatId: hatId.toString(), mask: a.mask, permissions: (0, perms_1.formatMask)(a.mask) },
386
+ },
387
+ };
388
+ }
389
+ /**
390
+ * The executor-gated setConfig(ROLE_PERM, abi.encode(uint256 hatId, uint8
391
+ * mask)) call `pop task perms propose-global` wraps in a proposal —
392
+ * src/commands/task/perms.ts:418. The CLI encodes via an inline
393
+ * `setConfig(uint8,bytes)` interface; the synced TaskManagerNew ABI carries
394
+ * the same signature, so the calldata is byte-identical.
395
+ */
396
+ function buildSetGlobalRolePermCall(taskManagerAddress, hatId, mask) {
397
+ const encodedValue = ethers_1.ethers.utils.defaultAbiCoder.encode(['uint256', 'uint8'], [ethers_1.ethers.BigNumber.from(hatId), mask]);
398
+ return (0, governance_1.encodeExecutorCall)('TaskManagerNew', taskManagerAddress, 'setConfig', [exports.CONFIG_KEY_ROLE_PERM, encodedValue]);
399
+ }
400
+ /**
401
+ * Port of `pop task perms propose-global` — src/commands/task/perms.ts:400.
402
+ * HybridVoting.createProposal(titleBytes, descriptionHash, duration, 2,
403
+ * [[setConfig call], []], []) — option 0 applies the mask, option 1 keeps
404
+ * current. Title matches the CLI's deterministic string.
405
+ */
406
+ function buildProposeGlobalRolePerm(a) {
407
+ const hatId = ethers_1.ethers.BigNumber.from(a.hatId);
408
+ const permsLabel = (0, perms_1.formatMask)(a.mask);
409
+ const title = `Set global task permissions for hat ${hatId.toString()} to ${permsLabel}`;
410
+ return (0, governance_1.buildGovernanceProposal)({
411
+ votingAddress: a.hybridVotingAddress,
412
+ votingAbiName: 'HybridVotingNew',
413
+ title,
414
+ descriptionHash: (0, encoding_1.ipfsCidToBytes32)(a.descriptionHash),
415
+ durationMinutes: a.durationMinutes,
416
+ numOptions: 2,
417
+ batches: [
418
+ [buildSetGlobalRolePermCall(a.taskManagerAddress, hatId, a.mask)], // option 0: apply
419
+ [], // option 1: keep current
420
+ ],
421
+ hatIds: [],
422
+ orgId: a.orgId,
423
+ domain: 'task',
424
+ action: 'perms-propose-global',
425
+ summary: { hatId: hatId.toString(), mask: a.mask, permissions: permsLabel },
426
+ });
427
+ }
428
+ /**
429
+ * Port of `pop task folders set` — src/commands/task/folders.ts:286
430
+ * (setFolders send). CAS-guarded: reverts FoldersRootStale when
431
+ * expectedCurrentRoot no longer matches; the CLI's read-retry-once loop is
432
+ * execution-time behavior and stays host-side.
433
+ */
434
+ function buildSetFolders(a) {
435
+ return {
436
+ to: a.taskManagerAddress,
437
+ abi: (0, contracts_1.getAbi)('TaskManagerNew'),
438
+ method: 'setFolders',
439
+ args: [a.expectedCurrentRoot, a.newRoot],
440
+ meta: {
441
+ domain: 'task',
442
+ action: 'folders-set',
443
+ orgId: a.orgId,
444
+ summary: { expectedCurrentRoot: a.expectedCurrentRoot, newRoot: a.newRoot },
445
+ },
446
+ };
447
+ }
448
+ async function resolveTaskManager(ctx, org) {
449
+ const modules = await (0, resolve_1.resolveOrgModules)(ctx.client, org, ctx.chainId);
450
+ return {
451
+ orgId: modules.orgId,
452
+ taskManagerAddress: (0, resolve_1.requireModule)(modules, 'taskManagerAddress'),
453
+ hybridVotingAddress: modules.hybridVotingAddress,
454
+ };
455
+ }
456
+ /**
457
+ * Feature probe for builders whose ENCODING depends on it. `features` override
458
+ * wins; otherwise detection needs ctx.provider (the CLI always has one — a
459
+ * subgraph-served implementation could pick the wrong calldata shape, see
460
+ * version.ts).
461
+ */
462
+ async function requireFeatures(ctx, taskManagerAddress, orgId, override, purpose) {
463
+ if (override)
464
+ return override;
465
+ if (!ctx.provider) {
466
+ throw new errors_1.CliError(`${purpose} needs the org's TaskManager feature set. Provide ctx.provider (for detection) or params.features.`, exit_codes_1.EXIT.USAGE);
467
+ }
468
+ return (0, version_1.detectTaskManagerFeatures)(ctx.provider, taskManagerAddress, ctx.chainId, {
469
+ orgId,
470
+ client: ctx.client,
471
+ });
472
+ }
473
+ /** Best-effort probe for gates that do not change the encoding (e.g. unclaim). */
474
+ async function maybeFeatures(ctx, taskManagerAddress, orgId, override) {
475
+ if (override)
476
+ return override;
477
+ if (!ctx.provider)
478
+ return null;
479
+ return (0, version_1.detectTaskManagerFeatures)(ctx.provider, taskManagerAddress, ctx.chainId, {
480
+ orgId,
481
+ client: ctx.client,
482
+ });
483
+ }
484
+ /**
485
+ * Read the org's payout convention, distinguishing "no pricing configured"
486
+ * (org row with null metadata ⇒ default pricing is correct) from "org not
487
+ * indexed / subgraph down" (refuse-to-guess). Port of the config read in
488
+ * task/create.ts:213 / create-batch.ts:167.
489
+ */
490
+ async function fetchOrgPayoutConfig(ctx, orgId) {
491
+ let config = {
492
+ hoursOnly: false, hourlyRate: null, useTokenSymbol: false,
493
+ };
494
+ let error = null;
495
+ try {
496
+ const cfg = await ctx.client.query(org_1.FETCH_ORG_PAYOUT_CONFIG, { orgId }, ctx.chainId);
497
+ if (!cfg.organization) {
498
+ // A successful query with NO org row (indexer lag on a fresh org) is
499
+ // indistinguishable from "no pricing configured" only by accident —
500
+ // treat it like a failed fetch so derivation refuses to guess.
501
+ throw new Error('organization not indexed');
502
+ }
503
+ config = (0, payout_1.payoutConfigFromMetadata)(cfg.organization?.metadata);
504
+ }
505
+ catch (err) {
506
+ // Only fatal when a payout must be DERIVED — explicit payouts do not
507
+ // depend on org pricing.
508
+ error = err;
509
+ }
510
+ return { config, error };
511
+ }
512
+ /**
513
+ * Resolve a project input the way `pop task create` does (task/create.ts:178):
514
+ * bytes32 hex as-is → subgraph title match → numeric index → error listing
515
+ * the available titles. NOTE the order differs from project delete's resolver
516
+ * (reads/project resolveProjectInput), which tries numerics before titles.
517
+ */
518
+ async function resolveCreateProjectPid(ctx, orgId, projectInput) {
519
+ if (projectInput.startsWith('0x') && projectInput.length === 66)
520
+ return projectInput;
521
+ const projResult = await ctx.client.query(task_1.FETCH_PROJECTS_DATA, { orgId }, ctx.chainId);
522
+ const projects = projResult.organization?.taskManager?.projects || [];
523
+ const match = projects.find((p) => (p.title || '').toLowerCase() === projectInput.toLowerCase());
524
+ if (match) {
525
+ // Extract bytes32 project ID from subgraph composite ID: "{contractAddress}-{projectIdHex}"
526
+ return (0, encoding_1.parseProjectId)(match.id);
527
+ }
528
+ const num = parseInt(projectInput, 10);
529
+ if (!isNaN(num)) {
530
+ return ethers_1.ethers.utils.hexZeroPad(ethers_1.ethers.utils.hexlify(num), 32);
531
+ }
532
+ const available = projects.map((p) => p.title).filter(Boolean).join(', ');
533
+ throw new Error(`Project "${projectInput}" not found. Available: ${available || 'none'}`);
534
+ }
535
+ /**
536
+ * Port of `pop task create` — src/commands/task/create.ts. Resolves the org +
537
+ * project, feature-gates the deadline flags, derives the payout exactly as
538
+ * the web app would when omitted, pins the canonical metadata document, and
539
+ * delegates to buildCreateTask. Duplicate-check and idempotency are CLI UX
540
+ * and are not ported.
541
+ */
542
+ async function createTaskIntent(ctx, p) {
543
+ // Parse deadline flags up-front so bad input fails before any network work.
544
+ const deadlineFlagsSet = p.deadline !== undefined || p.completionWindow !== undefined;
545
+ const absoluteDeadline = p.deadline !== undefined ? parseDeadlineInput(p.deadline) : 0;
546
+ const completionWindow = p.completionWindow !== undefined ? parseWindowInput(p.completionWindow) : 0;
547
+ const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
548
+ // Feature-gate: pre-v6 TaskManagers only expose the 7-arg createTask.
549
+ const features = await requireFeatures(ctx, taskManagerAddress, orgId, p.features, 'createTask');
550
+ if (!features.deadlines && deadlineFlagsSet) {
551
+ throw new errors_1.CliError((0, version_1.featureUnavailable)('task deadlines', 'TaskManager v6', 'Re-run without --deadline/--completion-window, or upgrade the org TaskManager beacon.'), exit_codes_1.EXIT.PRECONDITION);
552
+ }
553
+ const pid = await resolveCreateProjectPid(ctx, orgId, p.project);
554
+ // Price the task. TaskManager stores whatever payout it is handed, so pricing is a
555
+ // convention shared with the web app rather than an on-chain rule — when payout is
556
+ // omitted, derive it exactly as the UI does.
557
+ const { config: payoutConfig, error: payoutConfigError } = await fetchOrgPayoutConfig(ctx, orgId);
558
+ let derivedPayout = null;
559
+ if (p.payout === undefined) {
560
+ if (payoutConfigError) {
561
+ // Deriving from the hard-coded default config would silently misprice the
562
+ // task for any org with hours-only or custom hourly pricing — and the
563
+ // payout goes on-chain. Refuse rather than guess.
564
+ throw new errors_1.CliError('Could not read the org payout config (subgraph unreachable), so a payout cannot be derived safely.', exit_codes_1.EXIT.PRECONDITION, 'Pass --payout explicitly, or retry when the subgraph is reachable.');
565
+ }
566
+ derivedPayout = (0, payout_1.calculatePayout)(p.difficulty || 'medium', p.estHours || 0, payoutConfig);
567
+ if (derivedPayout <= 0) {
568
+ throw new errors_1.CliError('Could not derive a payout: the org pays by hours only and --est-hours is 0.', exit_codes_1.EXIT.USAGE, 'Pass --est-hours, or set --payout explicitly.');
569
+ }
570
+ }
571
+ const payoutAmount = p.payout ?? derivedPayout;
572
+ // Metadata JSON (key order must match the frontend exactly — see metadata/task).
573
+ const metadata = (0, task_3.buildTaskMetadata)({
574
+ name: p.name,
575
+ description: p.description,
576
+ location: p.location || '',
577
+ difficulty: p.difficulty || 'medium',
578
+ estHours: p.estHours || 0,
579
+ submission: '',
580
+ });
581
+ const cid = await (0, ipfs_1.pinJson)((0, task_3.serializeTaskMetadata)(metadata), ctx.ipfs);
582
+ const intent = buildCreateTask({
583
+ taskManagerAddress,
584
+ payout: payoutAmount,
585
+ title: p.name,
586
+ metadataHash: cid,
587
+ projectId: pid,
588
+ bountyToken: p.bountyToken,
589
+ bountyAmount: p.bountyAmount,
590
+ requiresApplication: p.requiresApplication,
591
+ absoluteDeadline,
592
+ completionWindow,
593
+ features,
594
+ orgId,
595
+ });
596
+ intent.meta.ipfs = { cid, metadata };
597
+ if (derivedPayout !== null && intent.meta.summary)
598
+ intent.meta.summary.derivedPayout = derivedPayout;
599
+ return intent;
600
+ }
601
+ /**
602
+ * Port of `pop task create-batch` — src/commands/task/create-batch.ts.
603
+ * Returns ONE createTasksBatch intent (all-or-nothing) on v6 orgs, or one
604
+ * legacy 7-arg createTask intent PER ROW on pre-v6 orgs (the CLI sends those
605
+ * sequentially; --continue-on-error is host-side execution policy). Per-row
606
+ * metadata is pinned in input order so receipt TaskCreated logs line up.
607
+ */
608
+ async function createTasksBatchIntents(ctx, p) {
609
+ // Batch-wide defaults parsed up-front so bad input fails before network work.
610
+ const defaultDeadline = p.deadline !== undefined ? parseDeadlineInput(p.deadline) : 0;
611
+ const defaultWindow = p.completionWindow !== undefined ? parseWindowInput(p.completionWindow) : 0;
612
+ const batchDeadlineSet = p.deadline !== undefined || p.completionWindow !== undefined;
613
+ const tasks = [];
614
+ for (let i = 0; i < p.tasks.length; i++) {
615
+ const task = p.tasks[i];
616
+ if (!task.name || !task.description) {
617
+ throw new errors_1.CliError(`Row ${i + 1}: Missing required fields: name, description`, exit_codes_1.EXIT.USAGE);
618
+ }
619
+ // Rows go through String() + parse exactly like the CLI (create-batch.ts:129).
620
+ const absoluteDeadline = task.deadline !== undefined
621
+ ? (0, encoding_1.parseDeadline)(String(task.deadline))
622
+ : defaultDeadline;
623
+ const completionWindowSecs = task.completionWindow !== undefined
624
+ ? (0, encoding_1.parseDurationSeconds)(String(task.completionWindow))
625
+ : defaultWindow;
626
+ tasks.push({
627
+ ...task,
628
+ payout: task.payout ?? 0,
629
+ payoutProvided: task.payout !== undefined,
630
+ absoluteDeadline,
631
+ completionWindowSecs,
632
+ deadlineSet: batchDeadlineSet || task.deadline !== undefined || task.completionWindow !== undefined,
633
+ });
634
+ }
635
+ // Client-side EmptyBatch guard (the v6 contract reverts on empty input).
636
+ if (tasks.length === 0) {
637
+ throw new errors_1.CliError('EmptyBatch: file parsed to 0 valid tasks — nothing to create.', exit_codes_1.EXIT.USAGE);
638
+ }
639
+ const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
640
+ // Same pricing convention as `pop task create`.
641
+ const { config: payoutConfig, error: payoutConfigError } = await fetchOrgPayoutConfig(ctx, orgId);
642
+ const needsDerivation = tasks.filter((t) => !t.payoutProvided);
643
+ if (needsDerivation.length > 0 && payoutConfigError) {
644
+ throw new errors_1.CliError(`Could not read the org payout config (subgraph unreachable), and ${needsDerivation.length} `
645
+ + `row(s) omit "payout": ${needsDerivation.map((t) => t.name).join(', ')}. `
646
+ + 'Add explicit payouts to those rows, or retry when the subgraph is reachable.', exit_codes_1.EXIT.PRECONDITION);
647
+ }
648
+ for (const t of tasks) {
649
+ if (!t.payoutProvided) {
650
+ t.payout = (0, payout_1.calculatePayout)(t.difficulty || 'medium', t.estHours || 0, payoutConfig);
651
+ }
652
+ }
653
+ const unpriced = tasks.filter((t) => !t.payoutProvided && t.payout <= 0);
654
+ if (unpriced.length > 0) {
655
+ throw new errors_1.CliError(`${unpriced.length} row(s) could not be priced (no "payout", and difficulty/estHours `
656
+ + `derive to 0): ${unpriced.map((t) => t.name).join(', ')}`, exit_codes_1.EXIT.USAGE);
657
+ }
658
+ const pid = (0, encoding_1.parseProjectId)(p.project);
659
+ const features = await requireFeatures(ctx, taskManagerAddress, orgId, p.features, 'createTasksBatch');
660
+ if (tasks.some((t) => t.deadlineSet) && !features.deadlines) {
661
+ throw new errors_1.CliError((0, version_1.featureUnavailable)('task deadlines', 'TaskManager v6', 'Re-run without --deadline/--completion-window, or upgrade the org TaskManager beacon.'), exit_codes_1.EXIT.PRECONDITION);
662
+ }
663
+ // Per-row metadata: exactly buildMetadata() from create-batch.ts, pinned in
664
+ // input order.
665
+ const pinned = [];
666
+ for (const row of tasks) {
667
+ const metadata = (0, task_3.buildTaskMetadata)({
668
+ name: row.name,
669
+ description: row.description,
670
+ location: row.location || '',
671
+ difficulty: row.difficulty || 'medium',
672
+ estHours: row.estHours || 0,
673
+ submission: '',
674
+ });
675
+ const cid = await (0, ipfs_1.pinJson)((0, task_3.serializeTaskMetadata)(metadata), ctx.ipfs);
676
+ pinned.push({ row, cid, metadata });
677
+ }
678
+ if (features.batchCreate) {
679
+ const intent = buildCreateTasksBatch({
680
+ taskManagerAddress,
681
+ projectId: pid,
682
+ tasks: pinned.map(({ row, cid }) => ({
683
+ payout: row.payout,
684
+ title: row.name,
685
+ metadataHash: cid,
686
+ bountyToken: row.bountyToken,
687
+ bountyAmount: row.bountyAmount,
688
+ requiresApplication: row.requiresApplication,
689
+ absoluteDeadline: row.absoluteDeadline,
690
+ completionWindow: row.completionWindowSecs,
691
+ })),
692
+ orgId,
693
+ });
694
+ intent.meta.ipfs = { cid: pinned[0].cid, metadata: pinned.map((x) => x.metadata) };
695
+ return [intent];
696
+ }
697
+ // Legacy pre-v6 org: per-task 7-arg createTask intents (sequential sends).
698
+ return pinned.map(({ row, cid, metadata }) => {
699
+ const intent = buildCreateTask({
700
+ taskManagerAddress,
701
+ payout: row.payout,
702
+ title: row.name,
703
+ metadataHash: cid,
704
+ projectId: pid,
705
+ bountyToken: row.bountyToken,
706
+ bountyAmount: row.bountyAmount,
707
+ requiresApplication: row.requiresApplication,
708
+ features: { deadlines: false },
709
+ orgId,
710
+ });
711
+ intent.meta.ipfs = { cid, metadata };
712
+ return intent;
713
+ });
714
+ }
715
+ /** Port of `pop task claim` — src/commands/task/claim.ts. */
716
+ async function claimTaskIntent(ctx, p) {
717
+ const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
718
+ return buildClaimTask({ taskManagerAddress, taskId: p.task, orgId });
719
+ }
720
+ /**
721
+ * Port of `pop task unclaim` — src/commands/task/unclaim.ts (TaskManager v7).
722
+ * Feature-gated like the CLI when a provider (or features override) is
723
+ * available; without either the gate is skipped — the encoding is identical
724
+ * and the contract is the authority. The self-vs-force consent tiering and
725
+ * the lens gate are host UX.
726
+ */
727
+ async function unclaimTaskIntent(ctx, p) {
728
+ const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
729
+ const features = await maybeFeatures(ctx, taskManagerAddress, orgId, p.features);
730
+ if (features && !features.unclaim) {
731
+ throw new errors_1.CliError((0, version_1.featureUnavailable)('releasing a claimed task', 'TaskManager v7', 'Upgrade the org\'s TaskManager beacon, or wait for the claim deadline to expire so it can be taken over.'), exit_codes_1.EXIT.PRECONDITION);
732
+ }
733
+ return buildUnclaimTask({ taskManagerAddress, taskId: p.task, orgId });
734
+ }
735
+ /**
736
+ * Port of `pop task submit` — src/commands/task/submit.ts. The submission is
737
+ * a FULL OVERWRITE of the task's metadata pointer, so the current document is
738
+ * read first (subgraph → IPFS fallback) and merged; when the base is
739
+ * unrecoverable this REFUSES (exit INFRA) rather than blanking the task's
740
+ * name/description/dueDate. Pre-flight (CLAIMED + claimer) is host UX.
741
+ */
742
+ async function submitTaskIntent(ctx, p) {
743
+ const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
744
+ const parsedTaskId = (0, encoding_1.parseTaskId)(p.task);
745
+ // Fetch existing metadata so the submission preserves it. Match on the
746
+ // PARSED numeric id, fall back to IPFS before giving up — same ladder as
747
+ // task update / edit-meta.
748
+ let subgraphTask = null;
749
+ try {
750
+ const taskData = await ctx.client.query(task_1.FETCH_PROJECTS_DATA, { orgId }, ctx.chainId);
751
+ subgraphTask = (0, task_2.findSubgraphTask)(taskData.organization?.taskManager?.projects || [], parsedTaskId);
752
+ }
753
+ catch { /* handled below */ }
754
+ let existingMeta = subgraphTask?.metadata || null;
755
+ if (!existingMeta && subgraphTask?.metadataHash) {
756
+ try {
757
+ existingMeta = await (0, ipfs_1.fetchJson)(subgraphTask.metadataHash, ctx.ipfs);
758
+ }
759
+ catch { /* handled below */ }
760
+ }
761
+ if (!existingMeta) {
762
+ throw new errors_1.CliError(`Task ${p.task} metadata is not indexed yet (subgraph lag) — submitting now would `
763
+ + 'overwrite the task\'s name, description and due date with blanks.', exit_codes_1.EXIT.INFRA, 'Retry in a few seconds. The submission itself is unaffected once metadata resolves.');
764
+ }
765
+ // Merge submission into existing metadata (exact field-by-field parity with
766
+ // submit.ts:141 — note estimatedHours parseFloat and the '' difficulty
767
+ // fallback, which differ from update/edit-meta's merge).
768
+ const submissionMetadata = (0, task_3.buildTaskMetadata)({
769
+ name: existingMeta?.name || '',
770
+ description: existingMeta?.description || '',
771
+ location: existingMeta?.location || '',
772
+ difficulty: existingMeta?.difficulty || '',
773
+ estHours: existingMeta?.estimatedHours ? parseFloat(existingMeta.estimatedHours) : 0,
774
+ submission: p.submission,
775
+ // The subgraph re-points task.metadata at THIS submission JSON, so a
776
+ // dueDate omitted here is gone for good. Appended last, only when set.
777
+ dueDate: existingMeta?.dueDate ? Math.floor(Number(existingMeta.dueDate)) : undefined,
778
+ });
779
+ const cid = await (0, ipfs_1.pinJson)((0, task_3.serializeTaskMetadata)(submissionMetadata), ctx.ipfs);
780
+ const intent = buildSubmitTask({ taskManagerAddress, taskId: parsedTaskId, submissionHash: cid, orgId });
781
+ intent.meta.ipfs = { cid, metadata: submissionMetadata };
782
+ return intent;
783
+ }
784
+ /** Port of `pop task review --action approve` — src/commands/task/review.ts. */
785
+ async function completeTaskIntent(ctx, p) {
786
+ const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
787
+ return buildCompleteTask({ taskManagerAddress, taskId: p.task, orgId });
788
+ }
789
+ /**
790
+ * Port of `pop task review --action reject` — src/commands/task/review.ts.
791
+ * Pins the `{rejection}` document, then builds rejectTask. (The CLI pins
792
+ * after pre-flight + confirm; core has neither, so the pin is the first step.)
793
+ */
794
+ async function rejectTaskIntent(ctx, p) {
795
+ const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
796
+ const rejectionMetadata = (0, task_3.buildTaskRejectionMetadata)(p.reason);
797
+ const cid = await (0, ipfs_1.pinJson)((0, task_3.serializeTaskRejectionMetadata)(rejectionMetadata), ctx.ipfs);
798
+ const intent = buildRejectTask({ taskManagerAddress, taskId: p.task, rejectionHash: cid, orgId });
799
+ intent.meta.ipfs = { cid, metadata: rejectionMetadata };
800
+ return intent;
801
+ }
802
+ /** Port of `pop task cancel` — src/commands/task/cancel.ts (DESTRUCTIVE; consent is host UX). */
803
+ async function cancelTaskIntent(ctx, p) {
804
+ const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
805
+ return buildCancelTask({ taskManagerAddress, taskId: p.task, orgId });
806
+ }
807
+ /**
808
+ * Port of `pop task assign` — src/commands/task/assign.ts. --username resolves
809
+ * via the same inline all-chains accounts query; --assignee is validated as an
810
+ * address. One of the two is required.
811
+ */
812
+ async function assignTaskIntent(ctx, p) {
813
+ let assignee;
814
+ if (p.username) {
815
+ const query = `{ accounts(where: { username: "${p.username}" }, first: 1) { id username } }`;
816
+ const results = await ctx.client.queryAllChains(query, {});
817
+ const account = results.map((r) => r.data?.accounts?.[0]).find(Boolean);
818
+ if (!account) {
819
+ throw new Error(`Username "${p.username}" not found. Check spelling or use --assignee with the address.`);
820
+ }
821
+ assignee = account.id;
822
+ }
823
+ else {
824
+ assignee = (0, validation_1.requireAddress)(p.assignee, 'assignee');
825
+ }
826
+ const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
827
+ return buildAssignTask({ taskManagerAddress, taskId: p.task, assignee, orgId });
828
+ }
829
+ /**
830
+ * Port of `pop task apply` — src/commands/task/apply.ts. Pins the
831
+ * `{notes, experience}` document, then builds applyForTask. The
832
+ * requiresApplication / duplicate-application pre-flights are host UX (the
833
+ * contract's NoApplicationRequired / AlreadyApplied decodes are authoritative).
834
+ */
835
+ async function applyForTaskIntent(ctx, p) {
836
+ const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
837
+ const applicationData = (0, task_3.buildTaskApplicationMetadata)({ notes: p.notes, experience: p.experience });
838
+ const cid = await (0, ipfs_1.pinJson)((0, task_3.serializeTaskApplicationMetadata)(applicationData), ctx.ipfs);
839
+ const intent = buildApplyForTask({ taskManagerAddress, taskId: p.task, applicationHash: cid, orgId });
840
+ intent.meta.ipfs = { cid, metadata: applicationData };
841
+ return intent;
842
+ }
843
+ /** Port of `pop task approve-app` — src/commands/task/approve-application.ts. */
844
+ async function approveApplicationIntent(ctx, p) {
845
+ const applicant = (0, validation_1.requireAddress)(p.applicant, 'applicant');
846
+ const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
847
+ return buildApproveApplication({ taskManagerAddress, taskId: p.task, applicant, orgId });
848
+ }
849
+ /**
850
+ * Port of `pop task update` — src/commands/task/update.ts. updateTask is a
851
+ * FULL OVERWRITE on-chain, so this reads the current state first (chain via
852
+ * the task lens for payout/bounty/deadlines — ctx.provider required; subgraph
853
+ * + IPFS for title/description/metadata), merges only the fields given, and
854
+ * re-pins metadata ONLY when name/description actually change. Refuses (INFRA)
855
+ * when the overwrite base is unrecoverable. A past deadline is accepted and
856
+ * flagged in meta.summary.deadlineIsPast.
857
+ */
858
+ async function updateTaskIntent(ctx, p) {
859
+ // 0. Fail fast on input problems before any network work.
860
+ const changed = ['payout', 'name', 'description', 'bountyToken', 'bountyAmount', 'deadline', 'completionWindow']
861
+ .filter((key) => p[key] !== undefined);
862
+ if (changed.length === 0) {
863
+ throw new errors_1.CliError('Nothing to update — pass at least one field flag.', exit_codes_1.EXIT.USAGE, 'Available: --payout, --name, --description, --bounty-token, --bounty-amount, --deadline, --completion-window');
864
+ }
865
+ let deadlineIsPast = false;
866
+ let newAbsoluteDeadline;
867
+ if (p.deadline !== undefined) {
868
+ const parsed = parseUpdateDeadline(p.deadline);
869
+ newAbsoluteDeadline = parsed.value;
870
+ deadlineIsPast = parsed.isPast;
871
+ }
872
+ const newCompletionWindow = p.completionWindow !== undefined
873
+ ? (0, encoding_1.parseDurationSeconds)(p.completionWindow)
874
+ : undefined;
875
+ // 1. Resolve org, gate on the v6 signature.
876
+ const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
877
+ const taskId = (0, encoding_1.parseTaskId)(p.task);
878
+ const features = await requireFeatures(ctx, taskManagerAddress, orgId, p.features, 'updateTask');
879
+ if (!features.deadlines) {
880
+ // Pre-v6 implementations expose a 6-field updateTask that no longer
881
+ // exists in the synced ABI — do NOT attempt it; degrade clearly.
882
+ throw new errors_1.CliError((0, version_1.featureUnavailable)('full task editing (updateTask)', 'TaskManager v6', 'Upgrade the org TaskManager beacon, or use pop task edit-meta if the org supports metadata-only edits.'), exit_codes_1.EXIT.PRECONDITION);
883
+ }
884
+ // 2. READ: current on-chain fields (authoritative via lens).
885
+ if (!ctx.provider) {
886
+ throw new errors_1.CliError('updateTask needs ctx.provider: the current payout/bounty/deadlines are read from the TaskManager lens (a full overwrite must preserve them).', exit_codes_1.EXIT.USAGE);
887
+ }
888
+ const current = await (0, task_lens_1.getTaskOnChain)(ctx.provider, taskManagerAddress, taskId);
889
+ if (current.status === task_lens_1.TASK_STATUS.COMPLETED || current.status === task_lens_1.TASK_STATUS.CANCELLED) {
890
+ throw new errors_1.CliError(`Task ${taskId} is ${(0, task_lens_1.taskStatusName)(current.status)} — terminal states are immutable on-chain.`, exit_codes_1.EXIT.PRECONDITION, 'Create a new task instead (pop task create).');
891
+ }
892
+ // 3. READ: current title/description/metadata (subgraph → IPFS fallback).
893
+ const metadataChanging = p.name !== undefined || p.description !== undefined;
894
+ let subgraphTask = null;
895
+ try {
896
+ const result = await ctx.client.query(task_1.FETCH_PROJECTS_DATA, { orgId }, ctx.chainId);
897
+ subgraphTask = (0, task_2.findSubgraphTask)(result.organization?.taskManager?.projects || [], taskId);
898
+ }
899
+ catch { /* handled below based on what the merge needs */ }
900
+ let metadata = subgraphTask?.metadata || null;
901
+ if (!metadata && subgraphTask?.metadataHash) {
902
+ try {
903
+ metadata = await (0, ipfs_1.fetchJson)(subgraphTask.metadataHash, ctx.ipfs);
904
+ }
905
+ catch { /* IPFS lag — handled below */ }
906
+ }
907
+ const currentName = metadata?.name ?? subgraphTask?.title ?? undefined;
908
+ const currentDescription = metadata?.description ?? undefined;
909
+ const currentMetadataHash = subgraphTask?.metadataHash ?? undefined;
910
+ // updateTask overwrites title + metadataHash even when only, say, payout
911
+ // changed — so the current values must be recoverable.
912
+ if (!metadataChanging && (currentName === undefined || !currentMetadataHash)) {
913
+ throw new errors_1.CliError(`Task ${taskId} metadata is not indexed yet (subgraph lag) — cannot preserve the current title/metadata through a full-overwrite update.`, exit_codes_1.EXIT.INFRA, 'Retry in a few seconds, or pass --name and --description explicitly.');
914
+ }
915
+ if (metadataChanging
916
+ && (p.name === undefined || p.description === undefined)
917
+ && (currentName === undefined || currentDescription === undefined)) {
918
+ throw new errors_1.CliError(`Task ${taskId} metadata is not indexed yet (subgraph lag) — cannot merge a partial metadata edit.`, exit_codes_1.EXIT.INFRA, 'Retry in a few seconds, or pass BOTH --name and --description.');
919
+ }
920
+ // 4. MERGE: given fields override, everything else preserved.
921
+ const finalPayout = p.payout !== undefined
922
+ ? ethers_1.ethers.utils.parseUnits(p.payout.toString(), 18)
923
+ : current.payout;
924
+ let finalBountyToken = current.bountyToken;
925
+ if (p.bountyToken !== undefined) {
926
+ if (p.bountyToken.toLowerCase() === 'none') {
927
+ finalBountyToken = ethers_1.ethers.constants.AddressZero;
928
+ }
929
+ else if (ethers_1.ethers.utils.isAddress(p.bountyToken)) {
930
+ finalBountyToken = ethers_1.ethers.utils.getAddress(p.bountyToken);
931
+ }
932
+ else {
933
+ throw new errors_1.CliError(`Invalid --bounty-token "${p.bountyToken}".`, exit_codes_1.EXIT.USAGE, 'Pass an ERC20 address, or "none" to clear the bounty.');
934
+ }
935
+ }
936
+ let finalBountyPayout = current.bountyPayout;
937
+ if (finalBountyToken === ethers_1.ethers.constants.AddressZero) {
938
+ finalBountyPayout = ethers_1.ethers.BigNumber.from(0); // zero token ⇒ zero payout (contract validation)
939
+ }
940
+ else if (p.bountyAmount !== undefined) {
941
+ finalBountyPayout = p.bountyAmount > 0
942
+ ? ethers_1.ethers.utils.parseUnits(p.bountyAmount.toString(), (0, chains_1.getTokenDecimals)(finalBountyToken))
943
+ : ethers_1.ethers.BigNumber.from(0);
944
+ }
945
+ // (CLI-only warning when the token changed without --bounty-amount: the raw
946
+ // current amount is kept, which may mean a different human value.)
947
+ const finalDeadline = newAbsoluteDeadline !== undefined ? newAbsoluteDeadline : (current.absoluteDeadline ?? 0);
948
+ const finalWindow = newCompletionWindow !== undefined ? newCompletionWindow : (current.completionWindow ?? 0);
949
+ const finalName = p.name ?? currentName ?? '';
950
+ const finalDescription = p.description ?? currentDescription ?? '';
951
+ // Re-pin only when the metadata content actually changes.
952
+ let finalMetadataHash = currentMetadataHash
953
+ ? (0, encoding_1.ipfsCidToBytes32)(currentMetadataHash)
954
+ : ethers_1.ethers.constants.HashZero;
955
+ let newCid;
956
+ let pinnedMetadata;
957
+ const metadataActuallyChanged = metadataChanging
958
+ && (finalName !== currentName || finalDescription !== currentDescription);
959
+ if (metadataActuallyChanged) {
960
+ // Key order MUST match task create (frontend parity) — see metadata/task.
961
+ // When the current doc could not be fetched, location/difficulty/estHours
962
+ // reset to defaults exactly as the CLI warns they will.
963
+ pinnedMetadata = (0, task_3.buildTaskMetadata)({
964
+ name: finalName,
965
+ description: finalDescription,
966
+ location: metadata?.location || '',
967
+ difficulty: metadata?.difficulty || 'medium',
968
+ estHours: metadata?.estimatedHours || metadata?.estHours || 0,
969
+ submission: metadata?.submission || '',
970
+ // Soft due date is metadata-borne; a re-pin that drops it deletes it.
971
+ dueDate: metadata?.dueDate ? Math.floor(Number(metadata.dueDate)) : undefined,
972
+ });
973
+ newCid = await (0, ipfs_1.pinJson)((0, task_3.serializeTaskMetadata)(pinnedMetadata), ctx.ipfs);
974
+ finalMetadataHash = (0, encoding_1.ipfsCidToBytes32)(newCid);
975
+ }
976
+ const intent = buildUpdateTask({
977
+ taskManagerAddress,
978
+ taskId,
979
+ payoutWei: finalPayout,
980
+ title: finalName,
981
+ metadataHash: finalMetadataHash,
982
+ bountyToken: finalBountyToken,
983
+ bountyPayoutWei: finalBountyPayout,
984
+ absoluteDeadline: finalDeadline,
985
+ completionWindow: finalWindow,
986
+ orgId,
987
+ });
988
+ if (newCid && pinnedMetadata)
989
+ intent.meta.ipfs = { cid: newCid, metadata: pinnedMetadata };
990
+ intent.meta.summary = {
991
+ ...intent.meta.summary,
992
+ fieldsChanged: changed.join(','),
993
+ ...(deadlineIsPast ? { deadlineIsPast: true } : {}),
994
+ };
995
+ return intent;
996
+ }
997
+ /**
998
+ * Port of `pop task edit-meta` — src/commands/task/edit-meta.ts
999
+ * (updateTaskMetadata; TaskManager v5+). Reads the current name/description
1000
+ * (subgraph → IPFS fallback), merges, and re-pins. Returns NULL when the merge
1001
+ * is a no-op — the CLI short-circuits with success and never sends. The
1002
+ * terminal-status read uses the lens when ctx.provider is present (parity);
1003
+ * without one, the contract's BadStatus revert is the authority.
1004
+ */
1005
+ async function editTaskMetadataIntent(ctx, p) {
1006
+ if (p.name === undefined && p.description === undefined) {
1007
+ throw new errors_1.CliError('Nothing to edit — pass --name and/or --description.', exit_codes_1.EXIT.USAGE);
1008
+ }
1009
+ const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
1010
+ const taskId = (0, encoding_1.parseTaskId)(p.task);
1011
+ // Feature-gate: updateTaskMetadata shipped with TaskManager v5.
1012
+ const features = await requireFeatures(ctx, taskManagerAddress, orgId, p.features, 'updateTaskMetadata');
1013
+ if (!features.editMeta) {
1014
+ throw new errors_1.CliError((0, version_1.featureUnavailable)('post-claim metadata editing', 'TaskManager v5', 'Upgrade the org TaskManager beacon, or recreate the task with the corrected metadata.'), exit_codes_1.EXIT.PRECONDITION);
1015
+ }
1016
+ // Authoritative status read (also proves the task exists) when a provider
1017
+ // is available.
1018
+ if (ctx.provider) {
1019
+ const current = await (0, task_lens_1.getTaskOnChain)(ctx.provider, taskManagerAddress, taskId);
1020
+ if (current.status === task_lens_1.TASK_STATUS.COMPLETED || current.status === task_lens_1.TASK_STATUS.CANCELLED) {
1021
+ throw new errors_1.CliError(`Task ${taskId} is ${(0, task_lens_1.taskStatusName)(current.status)} — terminal states are immutable on-chain.`, exit_codes_1.EXIT.PRECONDITION, 'Metadata of completed/cancelled tasks cannot be edited.');
1022
+ }
1023
+ }
1024
+ // READ current metadata: subgraph first, IPFS pointer as fallback.
1025
+ let subgraphTask = null;
1026
+ try {
1027
+ const result = await ctx.client.query(task_1.FETCH_PROJECTS_DATA, { orgId }, ctx.chainId);
1028
+ subgraphTask = (0, task_2.findSubgraphTask)(result.organization?.taskManager?.projects || [], taskId);
1029
+ }
1030
+ catch { /* handled below */ }
1031
+ let metadata = subgraphTask?.metadata || null;
1032
+ if (!metadata && subgraphTask?.metadataHash) {
1033
+ try {
1034
+ metadata = await (0, ipfs_1.fetchJson)(subgraphTask.metadataHash, ctx.ipfs);
1035
+ }
1036
+ catch { /* handled below */ }
1037
+ }
1038
+ const currentName = metadata?.name ?? subgraphTask?.title ?? undefined;
1039
+ const currentDescription = metadata?.description ?? undefined;
1040
+ if ((p.name === undefined && currentName === undefined)
1041
+ || (p.description === undefined && currentDescription === undefined)) {
1042
+ throw new errors_1.CliError(`Task ${taskId} metadata is not indexed yet (subgraph lag) — cannot merge a partial metadata edit.`, exit_codes_1.EXIT.INFRA, 'Retry in a few seconds, or pass BOTH --name and --description.');
1043
+ }
1044
+ // MERGE
1045
+ const finalName = p.name ?? currentName ?? '';
1046
+ const finalDescription = p.description ?? currentDescription ?? '';
1047
+ if (finalName === currentName && finalDescription === currentDescription) {
1048
+ // No-op: the CLI reports success without sending anything.
1049
+ return null;
1050
+ }
1051
+ // Key order MUST match task create (frontend parity) — see metadata/task.
1052
+ const metadataJson = (0, task_3.buildTaskMetadata)({
1053
+ name: finalName,
1054
+ description: finalDescription,
1055
+ location: metadata?.location || '',
1056
+ difficulty: metadata?.difficulty || 'medium',
1057
+ estHours: metadata?.estimatedHours || metadata?.estHours || 0,
1058
+ submission: metadata?.submission || '',
1059
+ // Soft due date is metadata-borne; a re-pin that drops it deletes it.
1060
+ dueDate: metadata?.dueDate ? Math.floor(Number(metadata.dueDate)) : undefined,
1061
+ });
1062
+ const cid = await (0, ipfs_1.pinJson)((0, task_3.serializeTaskMetadata)(metadataJson), ctx.ipfs);
1063
+ const intent = buildUpdateTaskMetadata({ taskManagerAddress, taskId, title: finalName, metadataHash: cid, orgId });
1064
+ intent.meta.ipfs = { cid, metadata: metadataJson };
1065
+ return intent;
1066
+ }
1067
+ /**
1068
+ * Port of `pop task perms set` — src/commands/task/perms.ts:309. Resolves the
1069
+ * project by hex or title (PERMS_QUERY_FULL/LEGACY tiers), parses the perm
1070
+ * list into a mask, and builds setProjectRolePerm. The creator-hat/executor
1071
+ * gate stays on-chain (masks are not readable per-wearer).
1072
+ */
1073
+ async function setProjectRolePermIntent(ctx, p) {
1074
+ const mask = typeof p.perms === 'number' ? p.perms : (0, perms_1.parsePermList)(p.perms);
1075
+ const hatId = parseHatId(p.hat);
1076
+ const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
1077
+ let pid;
1078
+ if (p.project.startsWith('0x') && p.project.length === 66) {
1079
+ pid = p.project;
1080
+ }
1081
+ else {
1082
+ const { data } = await ctx.client.queryWithFieldFallback((0, task_2.taskPermsTiers)(orgId), { chainId: ctx.chainId });
1083
+ pid = (0, task_2.resolveProjectFromList)(data.organization?.taskManager?.projects || [], p.project).pid;
1084
+ }
1085
+ return buildSetProjectRolePerm({ taskManagerAddress, projectId: pid, hatId, mask, orgId });
1086
+ }
1087
+ /**
1088
+ * Port of `pop task perms propose-global` — src/commands/task/perms.ts:400.
1089
+ * setConfig(ROLE_PERM) is executor-only, so the change ships as a HybridVoting
1090
+ * proposal whose option-0 batch targets the TaskManager. Pins the proposal
1091
+ * metadata {description, optionNames, createdAt} (exact key order), then
1092
+ * delegates to buildProposeGlobalRolePerm.
1093
+ */
1094
+ async function proposeGlobalRolePermIntent(ctx, p) {
1095
+ const mask = typeof p.perms === 'number' ? p.perms : (0, perms_1.parsePermList)(p.perms);
1096
+ const hatId = parseHatId(p.hat);
1097
+ const duration = p.duration ?? 60;
1098
+ const { orgId, taskManagerAddress, hybridVotingAddress } = await resolveTaskManager(ctx, p.org);
1099
+ if (!hybridVotingAddress) {
1100
+ throw new errors_1.CliError('HybridVoting not deployed for this org — cannot create a governance proposal.', exit_codes_1.EXIT.PRECONDITION);
1101
+ }
1102
+ const permsLabel = (0, perms_1.formatMask)(mask);
1103
+ const title = `Set global task permissions for hat ${hatId.toString()} to ${permsLabel}`;
1104
+ // Proposal metadata — key order {description, optionNames, createdAt} is a
1105
+ // protocol contract (same shape every governance wrap pins).
1106
+ const metadata = (0, proposal_1.buildProposalMetadata)({
1107
+ description: `Set the GLOBAL TaskPerm mask for hat ${hatId.toString()} to ${mask} (${permsLabel}) via TaskManager.setConfig(ROLE_PERM). ${(0, perms_1.describeMask)(mask).join('; ') || 'Revokes all global task permissions for this hat.'}`,
1108
+ optionNames: [title, 'Keep current permissions'],
1109
+ });
1110
+ const cid = await (0, ipfs_1.pinJson)((0, proposal_1.serializeProposalMetadata)(metadata), ctx.ipfs);
1111
+ const intent = buildProposeGlobalRolePerm({
1112
+ hybridVotingAddress,
1113
+ taskManagerAddress,
1114
+ hatId,
1115
+ mask,
1116
+ durationMinutes: duration,
1117
+ descriptionHash: cid,
1118
+ orgId,
1119
+ });
1120
+ intent.meta.ipfs = { cid, metadata };
1121
+ return intent;
1122
+ }
1123
+ /**
1124
+ * Port of `pop task folders set` — src/commands/task/folders.ts:286
1125
+ * (TaskManager v4+). Parses the new root, auto-fills the CAS expected root
1126
+ * from chain, and returns NULL when the root already matches (the CLI reports
1127
+ * success without sending). The stale-root retry-once loop is execution-time
1128
+ * behavior and stays host-side.
1129
+ */
1130
+ async function setFoldersIntent(ctx, p) {
1131
+ const newRoot = parseFoldersRoot(p.newRoot);
1132
+ const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
1133
+ const features = await maybeFeatures(ctx, taskManagerAddress, orgId, p.features);
1134
+ if (features && !features.folders) {
1135
+ throw new errors_1.CliError((0, version_1.featureUnavailable)('task folders', 'TaskManager v4', 'Upgrade the org TaskManager beacon to use folder organization.'), exit_codes_1.EXIT.PRECONDITION);
1136
+ }
1137
+ let expectedRoot = p.expectedCurrentRoot;
1138
+ if (expectedRoot === undefined) {
1139
+ if (!ctx.provider) {
1140
+ throw new errors_1.CliError('setFolders needs the current root for its CAS guard. Provide ctx.provider (read over RPC) or params.expectedCurrentRoot.', exit_codes_1.EXIT.USAGE);
1141
+ }
1142
+ expectedRoot = await (0, task_lens_1.getFoldersRoot)(ctx.provider, taskManagerAddress);
1143
+ }
1144
+ if (expectedRoot.toLowerCase() === newRoot.toLowerCase()) {
1145
+ // Already up to date — the CLI reports success without sending.
1146
+ return null;
1147
+ }
1148
+ return buildSetFolders({ taskManagerAddress, expectedCurrentRoot: expectedRoot, newRoot, orgId });
1149
+ }