@pipefy/pipefy-process-coder 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.
- package/.agents/skills/ppc-pipefy-flow-authoring/SKILL.md +262 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-danfe-consulta.json +247 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-webhook-retorno-consulta.json +589 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-4-recebimento-barramento.json +1391 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-2-danfe-retorno.json +623 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-4-criacao-operacao.json +636 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/03-subflow-4-criacao-titulo.json +3642 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-cedente.json +863 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-sacado.json +799 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/README.md +42 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-acompanhamento-cobranca.json +581 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-nfe-monitoramento.json +503 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-retorno-bancario.json +562 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-cedente.json +557 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-sacado.json +609 -0
- package/.agents/skills/ppc-pipefy-pipe-authoring/SKILL.md +146 -0
- package/.agents/skills/ppc-pipefy-process-design/SKILL.md +127 -0
- package/.agents/skills/ppc-pipefy-workspace/SKILL.md +91 -0
- package/AGENTS.md +456 -0
- package/README.md +808 -0
- package/bin/pipe.js +13 -0
- package/package.json +35 -0
- package/src/apply/adopt.ts +150 -0
- package/src/apply/agentops.ts +71 -0
- package/src/apply/compile.ts +875 -0
- package/src/apply/execute.ts +336 -0
- package/src/apply/flowops.ts +399 -0
- package/src/apply/idmap.ts +241 -0
- package/src/apply/mutations.ts +955 -0
- package/src/apply/registry.ts +430 -0
- package/src/apply/types.ts +134 -0
- package/src/cli/args.ts +88 -0
- package/src/cli.ts +211 -0
- package/src/codec/flow.ts +199 -0
- package/src/codec/pack.ts +103 -0
- package/src/codec/roundtrip.ts +94 -0
- package/src/codec/unpack.ts +198 -0
- package/src/commands/agents.ts +184 -0
- package/src/commands/apply.ts +1144 -0
- package/src/commands/context.ts +119 -0
- package/src/commands/create.ts +79 -0
- package/src/commands/diff.ts +314 -0
- package/src/commands/flows.ts +414 -0
- package/src/commands/misc.ts +644 -0
- package/src/commands/plan.ts +331 -0
- package/src/commands/pull.ts +567 -0
- package/src/commands/runs.ts +83 -0
- package/src/commands/skills.ts +137 -0
- package/src/commands/verify.ts +253 -0
- package/src/config.ts +168 -0
- package/src/diff/agents.ts +122 -0
- package/src/diff/diff.ts +1130 -0
- package/src/diff/flow.ts +318 -0
- package/src/diff/html.ts +322 -0
- package/src/diff/render.ts +101 -0
- package/src/model/payload.ts +154 -0
- package/src/model/tree.ts +99 -0
- package/src/model/volatile.ts +55 -0
- package/src/pipefy/agents.ts +165 -0
- package/src/pipefy/automations.ts +219 -0
- package/src/pipefy/capability.ts +119 -0
- package/src/pipefy/client.ts +267 -0
- package/src/pipefy/discovery.ts +209 -0
- package/src/pipefy/internal.ts +380 -0
- package/src/pipefy/ipaas.ts +365 -0
- package/src/pipefy/reconstruct.ts +775 -0
- package/src/pipefy/reference.ts +251 -0
- package/src/pipefy/snapshot.ts +245 -0
- package/src/pipefy/toolkit.ts +200 -0
- package/src/pipefy/toolkit_bearer.py +137 -0
- package/src/report/integrations.ts +231 -0
- package/src/report/run.ts +475 -0
- package/src/util/fsx.ts +45 -0
- package/src/util/git.ts +32 -0
- package/src/util/json.ts +55 -0
- package/src/util/log.ts +76 -0
- package/src/util/pool.ts +48 -0
- package/src/util/slug.ts +26 -0
- package/src/util/tui.ts +335 -0
- package/src/validate/index.ts +123 -0
- package/src/validate/integrity.ts +387 -0
- package/src/validate/reference.ts +136 -0
- package/src/validate/schema.ts +328 -0
- package/src/workspace/agents.ts +290 -0
- package/src/workspace/docs.ts +407 -0
- package/src/workspace/flows.ts +191 -0
- package/src/workspace/layout.ts +165 -0
- package/src/workspace/lock.ts +148 -0
- package/src/workspace/read.ts +165 -0
- package/src/workspace/reference.ts +24 -0
- package/src/workspace/stamp.ts +301 -0
- package/src/workspace/write.ts +225 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { rel, type EntityName, type PayloadEnvelope, type RepoPayload, type Row } from '../model/payload.ts';
|
|
2
|
+
import { stripVolatile } from '../model/volatile.ts';
|
|
3
|
+
import type { AutomationNode, ConditionNode, FieldConditionNode, PhaseNode, Tree } from '../model/tree.ts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* payload → tree.
|
|
7
|
+
*
|
|
8
|
+
* Folding is *movement only*: a folded row is the payload row, unchanged, moved
|
|
9
|
+
* under its owner. The only rewriting is (a) stripping volatile columns and
|
|
10
|
+
* (b) dropping the owner-pointer columns that folding makes redundant, both of
|
|
11
|
+
* which `pack` restores. That is what makes `pack(unpack(P)) ≡ P` a property of
|
|
12
|
+
* the design rather than of the care taken.
|
|
13
|
+
*
|
|
14
|
+
* Keys beginning with `_` are tool metadata, not pipe structure. They survive a
|
|
15
|
+
* round trip and are ignored by every comparator.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const idOf = (r: Row): string => String(r['id'] ?? '');
|
|
19
|
+
|
|
20
|
+
const num = (v: unknown): number | null => {
|
|
21
|
+
if (v === null || v === undefined || v === '') return null;
|
|
22
|
+
const n = Number(v);
|
|
23
|
+
return Number.isFinite(n) ? n : null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const byIndex = (a: Row, b: Row) => {
|
|
27
|
+
const ai = num(a['index']);
|
|
28
|
+
const bi = num(b['index']);
|
|
29
|
+
if (ai !== null && bi !== null && ai !== bi) return ai - bi;
|
|
30
|
+
return (num(a['id']) ?? 0) - (num(b['id']) ?? 0);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Which key a phase_team_memberships row uses for the user id. */
|
|
34
|
+
const teamMemberKey = (row: Row): string | null => {
|
|
35
|
+
for (const k of ['user_id', 'member_id', 'team_member_id', 'user']) {
|
|
36
|
+
if (k in row && num(row[k]) !== null) return k;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Attach a condition to its owner. `conditions` rows point at their owner with
|
|
43
|
+
* (source_type, source_id); expressions point at the condition with
|
|
44
|
+
* `condition_id`, except in a reconstructed payload where no condition id
|
|
45
|
+
* exists and `_condition_owner` carries the same information.
|
|
46
|
+
*/
|
|
47
|
+
const takeCondition = (
|
|
48
|
+
payload: RepoPayload,
|
|
49
|
+
sourceType: 'Automation' | 'FieldCondition',
|
|
50
|
+
sourceId: string,
|
|
51
|
+
): ConditionNode | null => {
|
|
52
|
+
const cond = rel(payload, 'conditions').find(
|
|
53
|
+
(c) => String(c['source_type']) === sourceType && String(c['source_id']) === sourceId,
|
|
54
|
+
);
|
|
55
|
+
if (!cond) return null;
|
|
56
|
+
|
|
57
|
+
const condId = idOf(cond);
|
|
58
|
+
const expressions = rel(payload, 'condition_expressions')
|
|
59
|
+
.filter((e) => {
|
|
60
|
+
if (condId && String(e['condition_id'] ?? '') === condId) return true;
|
|
61
|
+
const owner = e['_condition_owner'] as Row | undefined;
|
|
62
|
+
return Boolean(owner && String(owner['source_type']) === sourceType && String(owner['source_id']) === sourceId);
|
|
63
|
+
})
|
|
64
|
+
.map((e) => {
|
|
65
|
+
const row = stripVolatile({ ...e });
|
|
66
|
+
// Restored by pack from the owning condition.
|
|
67
|
+
delete row['condition_id'];
|
|
68
|
+
delete row['_condition_owner'];
|
|
69
|
+
return row;
|
|
70
|
+
})
|
|
71
|
+
.sort((a, b) => (num(a['structure_id']) ?? 0) - (num(b['structure_id']) ?? 0) || (num(a['id']) ?? 0) - (num(b['id']) ?? 0));
|
|
72
|
+
|
|
73
|
+
const node = stripVolatile({ ...cond }) as ConditionNode;
|
|
74
|
+
// Restored by pack from the owner it is nested under.
|
|
75
|
+
delete (node as Row)['source_type'];
|
|
76
|
+
delete (node as Row)['source_id'];
|
|
77
|
+
node.expressions = expressions;
|
|
78
|
+
return node;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const unpack = (env: PayloadEnvelope): Tree => {
|
|
82
|
+
const p = env.payload;
|
|
83
|
+
|
|
84
|
+
const root: Row = {};
|
|
85
|
+
for (const [k, v] of Object.entries(p)) {
|
|
86
|
+
if (k === 'relations') continue;
|
|
87
|
+
root[k] = v;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const fieldsByPhase = new Map<string, Row[]>();
|
|
91
|
+
for (const f of rel(p, 'fields')) {
|
|
92
|
+
const key = String(f['phase_id'] ?? '');
|
|
93
|
+
const arr = fieldsByPhase.get(key) ?? [];
|
|
94
|
+
arr.push(f);
|
|
95
|
+
fieldsByPhase.set(key, arr);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const phases: PhaseNode[] = [...rel(p, 'phases')].sort(byIndex).map((raw) => {
|
|
99
|
+
const phaseId = idOf(raw);
|
|
100
|
+
const node = stripVolatile({ ...raw }) as PhaseNode;
|
|
101
|
+
|
|
102
|
+
node.fields = (fieldsByPhase.get(phaseId) ?? [])
|
|
103
|
+
.slice()
|
|
104
|
+
.sort(byIndex)
|
|
105
|
+
.map((f) => stripVolatile({ ...f }));
|
|
106
|
+
|
|
107
|
+
node.jump_target_ids = rel(p, 'phase_jumps')
|
|
108
|
+
.filter((j) => String(j['source_phase_id']) === phaseId)
|
|
109
|
+
.map((j) => num(j['target_phase_id']))
|
|
110
|
+
.filter((v): v is number => v !== null)
|
|
111
|
+
.sort((a, b) => a - b);
|
|
112
|
+
|
|
113
|
+
const memberships = rel(p, 'phase_team_memberships').filter((m) => String(m['phase_id']) === phaseId);
|
|
114
|
+
const key = memberships.length ? teamMemberKey(memberships[0] as Row) : null;
|
|
115
|
+
node.team_member_ids = key
|
|
116
|
+
? memberships.map((m) => num(m[key])).filter((v): v is number => v !== null).sort((a, b) => a - b)
|
|
117
|
+
: [];
|
|
118
|
+
if (key && key !== 'user_id') (node as Row)['_team_member_key'] = key;
|
|
119
|
+
|
|
120
|
+
return node;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const automations: AutomationNode[] = rel(p, 'automations').map((raw) => {
|
|
124
|
+
const id = idOf(raw);
|
|
125
|
+
const node = stripVolatile({ ...raw }) as AutomationNode;
|
|
126
|
+
node.condition = takeCondition(p, 'Automation', id);
|
|
127
|
+
node.field_maps = rel(p, 'field_maps')
|
|
128
|
+
.filter((m) => String(m['owner_type']) === 'Automation' && String(m['owner_id']) === id)
|
|
129
|
+
.map((m) => {
|
|
130
|
+
const row = stripVolatile({ ...m });
|
|
131
|
+
delete row['owner_type'];
|
|
132
|
+
delete row['owner_id'];
|
|
133
|
+
return row;
|
|
134
|
+
});
|
|
135
|
+
node.distribute_assignments = rel(p, 'distribute_assignments')
|
|
136
|
+
.filter((d) => String(d['automation_id'] ?? d['owner_id'] ?? '') === id)
|
|
137
|
+
.map((d) => {
|
|
138
|
+
const row = stripVolatile({ ...d });
|
|
139
|
+
delete row['automation_id'];
|
|
140
|
+
return row;
|
|
141
|
+
});
|
|
142
|
+
node.response_schemas = rel(p, 'response_schemas')
|
|
143
|
+
.filter((s) => String(s['automation_id'] ?? s['owner_id'] ?? '') === id)
|
|
144
|
+
.map((s) => {
|
|
145
|
+
const row = stripVolatile({ ...s });
|
|
146
|
+
delete row['automation_id'];
|
|
147
|
+
return row;
|
|
148
|
+
});
|
|
149
|
+
return node;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const field_conditions: FieldConditionNode[] = [...rel(p, 'field_conditions')].sort(byIndex).map((raw) => {
|
|
153
|
+
const id = idOf(raw);
|
|
154
|
+
const node = stripVolatile({ ...raw }) as FieldConditionNode;
|
|
155
|
+
node.actions = rel(p, 'field_condition_actions')
|
|
156
|
+
.filter((a) => String(a['field_condition_id']) === id)
|
|
157
|
+
.map((a) => {
|
|
158
|
+
const row = stripVolatile({ ...a });
|
|
159
|
+
delete row['field_condition_id'];
|
|
160
|
+
return row;
|
|
161
|
+
});
|
|
162
|
+
node.condition = takeCondition(p, 'FieldCondition', id);
|
|
163
|
+
return node;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const simple = (n: EntityName): Row[] => rel(p, n).map((r) => stripVolatile({ ...r }));
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
meta: {
|
|
170
|
+
source: env.source,
|
|
171
|
+
versionId: env.versionId ?? null,
|
|
172
|
+
readAt: env.readAt,
|
|
173
|
+
repoKind: env.repoKind,
|
|
174
|
+
coverage: env.coverage,
|
|
175
|
+
repo_columns: env.repo_columns ?? 'complete',
|
|
176
|
+
notes: env.notes,
|
|
177
|
+
},
|
|
178
|
+
repo: stripVolatile(root),
|
|
179
|
+
phases,
|
|
180
|
+
automations,
|
|
181
|
+
field_conditions,
|
|
182
|
+
labels: simple('labels'),
|
|
183
|
+
webhooks: simple('webhooks'),
|
|
184
|
+
pipe_relations: simple('pipe_relations'),
|
|
185
|
+
public_form: simple('public_forms')[0] ?? null,
|
|
186
|
+
preferences: simple('repo_preferences')[0] ?? null,
|
|
187
|
+
readonly: {
|
|
188
|
+
email_templates: simple('email_templates'),
|
|
189
|
+
email_inboxes: simple('email_inboxes'),
|
|
190
|
+
visibilities: simple('visibilities'),
|
|
191
|
+
// owner_type/owner_id kept — see model/tree.ts. The automation-owned half
|
|
192
|
+
// above already claimed and stripped its rows; this is everything left.
|
|
193
|
+
field_maps: rel(p, 'field_maps')
|
|
194
|
+
.filter((m) => String(m['owner_type']) !== 'Automation')
|
|
195
|
+
.map((m) => stripVolatile({ ...m })),
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { info, dim, ok, warn, debug } from '../util/log.ts';
|
|
2
|
+
import { fetchAgents } from '../pipefy/agents.ts';
|
|
3
|
+
import type { PipefyClient } from '../pipefy/client.ts';
|
|
4
|
+
import {
|
|
5
|
+
writeAgents,
|
|
6
|
+
writeAgentBaseline,
|
|
7
|
+
readAgents,
|
|
8
|
+
readAgentBaseline,
|
|
9
|
+
unknownAgents,
|
|
10
|
+
agentDirName,
|
|
11
|
+
type AgentsSection,
|
|
12
|
+
} from '../workspace/agents.ts';
|
|
13
|
+
import { readTree } from '../workspace/read.ts';
|
|
14
|
+
import { diffAgents, groupBehaviorsByAgentDir, type AgentChangeSet } from '../diff/agents.ts';
|
|
15
|
+
import { compileAgents, type AgentPlan, type AgentStep } from '../apply/agentops.ts';
|
|
16
|
+
import type { Tree } from '../model/tree.ts';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The agent half of the pipe loop: read, diff, plan, apply.
|
|
20
|
+
*
|
|
21
|
+
* A behavior's own edits already flow through the ordinary automation
|
|
22
|
+
* diff/plan/apply once it is placed under its agent's `behaviors/`
|
|
23
|
+
* (workspace/agents.ts) — the row is folded into `tree.automations` like any
|
|
24
|
+
* other automation. But *sending* one is not the automation mutations: this
|
|
25
|
+
* pipe's action catalogue does not offer `ai_behavior` to `createAutomation`
|
|
26
|
+
* (confirmed live, refused), and the confirmed write path — captured from
|
|
27
|
+
* real Pipefy web UI traffic and reproduced directly — is a single
|
|
28
|
+
* `createAiAgent` / `updateAiAgent` call carrying the agent's *entire*
|
|
29
|
+
* behaviors list. So `diff/diff.ts` and `apply/compile.ts` are taught to skip
|
|
30
|
+
* any automation tagged `_agent_dir`, and this module is the whole agent-side
|
|
31
|
+
* pipeline instead: read the agent's fields plus its current behaviors
|
|
32
|
+
* (grouped from the already-read tree, since `agent.json` never carries them),
|
|
33
|
+
* diff against the baseline, compile straight to `createAiAgent` /
|
|
34
|
+
* `updateAiAgent` / `deleteAiAgent`, and send.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/** Read a pipe's AI agents, or record why they could not be read. */
|
|
38
|
+
export const pullAgentsFor = async (client: PipefyClient, repoUuid: string | null | undefined): Promise<AgentsSection> => {
|
|
39
|
+
if (!repoUuid) return unknownAgents('this repo has no uuid on the row that was read, so agents cannot be looked up');
|
|
40
|
+
|
|
41
|
+
const read = await fetchAgents(client, repoUuid);
|
|
42
|
+
return {
|
|
43
|
+
meta: {
|
|
44
|
+
coverage: read.status,
|
|
45
|
+
readAt: new Date().toISOString(),
|
|
46
|
+
agents: read.status === 'complete' ? read.agents.length : undefined,
|
|
47
|
+
notes: read.note ? [read.note] : [],
|
|
48
|
+
},
|
|
49
|
+
agents: read.agents,
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Pull and write, mirroring `pullAndWriteFlows`. Returns what was read. */
|
|
54
|
+
export const pullAndWriteAgents = async (
|
|
55
|
+
client: PipefyClient,
|
|
56
|
+
repoId: number,
|
|
57
|
+
repoUuid: string | null | undefined,
|
|
58
|
+
repoDir: string,
|
|
59
|
+
opts: { workspaceRoot?: string } = {},
|
|
60
|
+
): Promise<AgentsSection> => {
|
|
61
|
+
const section = await pullAgentsFor(client, repoUuid);
|
|
62
|
+
await writeAgents(repoDir, section);
|
|
63
|
+
if (opts.workspaceRoot) await writeAgentBaseline(opts.workspaceRoot, repoId, section);
|
|
64
|
+
return section;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The agent diff: baseline against the workspace's own files.
|
|
69
|
+
*
|
|
70
|
+
* `tree`, if the caller already read one for the pipe half, is reused rather
|
|
71
|
+
* than reading the whole workspace a second time — `agent.json` alone cannot
|
|
72
|
+
* answer "what are this agent's current behaviors", only `tree.automations`
|
|
73
|
+
* (filtered to what `_agent_dir` tags) can.
|
|
74
|
+
*/
|
|
75
|
+
export const diffAgentsFor = async (
|
|
76
|
+
root: string,
|
|
77
|
+
repoId: number,
|
|
78
|
+
dir: string,
|
|
79
|
+
opts: { tree?: Tree } = {},
|
|
80
|
+
): Promise<AgentChangeSet> => {
|
|
81
|
+
const baseline = await readAgentBaseline(root, repoId);
|
|
82
|
+
const authored = await readAgents(dir);
|
|
83
|
+
const tree = opts.tree ?? (await readTree(dir));
|
|
84
|
+
const behaviorsByDir = groupBehaviorsByAgentDir(tree.automations);
|
|
85
|
+
return diffAgents(baseline, authored, behaviorsByDir);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const planAgents = (client: PipefyClient | null, cs: AgentChangeSet): Promise<AgentPlan> => compileAgents(client, cs);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The revert change set, given both sides already read — the pure half of
|
|
92
|
+
* `planAgentRevert`, mirroring `buildFlowRevertPlan` (commands/flows.ts).
|
|
93
|
+
*
|
|
94
|
+
* A live agent read is self-contained — `AgentRow.behaviors` carries the full
|
|
95
|
+
* list (pipefy/agents.ts), unlike the authored-files side, which needs a
|
|
96
|
+
* whole tree to answer the same question because a behavior's file lives
|
|
97
|
+
* under its agent's `behaviors/` folder, not inside `agent.json`. So `before`
|
|
98
|
+
* alone is enough to build the map `diffAgents` needs for the *target* side.
|
|
99
|
+
*
|
|
100
|
+
* Direction is the mirror of an ordinary apply: `live` is the current state
|
|
101
|
+
* (baseline), `before` is the old pre-apply one (intended target).
|
|
102
|
+
*/
|
|
103
|
+
export const buildAgentRevertChangeSet = (live: AgentsSection, before: AgentsSection): AgentChangeSet => {
|
|
104
|
+
const behaviorsByDir = new Map(before.agents.map((a) => [agentDirName(a), a.behaviors ?? []]));
|
|
105
|
+
return diffAgents(live, before, behaviorsByDir);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* What reverting to a pre-apply agent state would take — computed, not sent.
|
|
110
|
+
*
|
|
111
|
+
* `before` is a live read taken right before this apply's own agent diff was
|
|
112
|
+
* computed — the only rollback point agents have, since they are not part of
|
|
113
|
+
* the snapshot payload at all (ROUTE-B-V2.md ask 5) and their diff is
|
|
114
|
+
* ordinarily computed offline against the local baseline, which never took
|
|
115
|
+
* this read on its own. `live` here is read fresh, because some steps may
|
|
116
|
+
* have landed since `before` was taken.
|
|
117
|
+
*/
|
|
118
|
+
export const planAgentRevert = async (
|
|
119
|
+
client: PipefyClient,
|
|
120
|
+
repoUuid: string | null | undefined,
|
|
121
|
+
before: AgentsSection,
|
|
122
|
+
): Promise<{ plan: AgentPlan; live: AgentsSection }> => {
|
|
123
|
+
const live = await pullAgentsFor(client, repoUuid);
|
|
124
|
+
const changeSet = buildAgentRevertChangeSet(live, before);
|
|
125
|
+
return { plan: await planAgents(client, changeSet), live };
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export const renderAgentPlan = (plan: AgentPlan): string => {
|
|
129
|
+
const lines: string[] = [];
|
|
130
|
+
if (!plan.steps.length) {
|
|
131
|
+
lines.push(' agents: nothing to apply');
|
|
132
|
+
return lines.join('\n');
|
|
133
|
+
}
|
|
134
|
+
lines.push('');
|
|
135
|
+
lines.push(` ${plan.steps.length} agent step(s)`);
|
|
136
|
+
for (const s of plan.steps) {
|
|
137
|
+
lines.push(` ${s.id} ${s.description}${s.destructive ? ' destructive' : ''}`);
|
|
138
|
+
}
|
|
139
|
+
return lines.join('\n');
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export type AgentApplyResult = { done: number; failed: number; skipped: number; steps: AgentStep[] };
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Execute the agent steps.
|
|
146
|
+
*
|
|
147
|
+
* No resumability, no placeholder substitution — unlike the pipe half's
|
|
148
|
+
* executor, an agent step never depends on an id created earlier in the same
|
|
149
|
+
* plan (an agent's own uuid is not referenced by anything else this tool
|
|
150
|
+
* writes), so the machinery `apply/execute.ts` needs for that would be
|
|
151
|
+
* unused weight here. Same simplification the flow half already makes.
|
|
152
|
+
*/
|
|
153
|
+
export const applyAgentSteps = async (
|
|
154
|
+
client: PipefyClient,
|
|
155
|
+
plan: AgentPlan,
|
|
156
|
+
opts: { dryRun?: boolean; continueOnError?: boolean } = {},
|
|
157
|
+
): Promise<AgentApplyResult> => {
|
|
158
|
+
for (const step of plan.steps) {
|
|
159
|
+
if (opts.dryRun) {
|
|
160
|
+
step.status = 'skipped';
|
|
161
|
+
info(dim(` (dry run) ${step.id} ${step.description} — not sent`));
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
step.status = 'running';
|
|
166
|
+
try {
|
|
167
|
+
if (step.request.missingRequired?.length) {
|
|
168
|
+
throw new Error(`missing required field(s): ${step.request.missingRequired.join(', ')}`);
|
|
169
|
+
}
|
|
170
|
+
debug(JSON.stringify(step.request.variables ?? {}).slice(0, 400));
|
|
171
|
+
step.result = await client.raw(step.request.query ?? '', step.request.variables, step.description);
|
|
172
|
+
step.status = 'done';
|
|
173
|
+
ok(`${step.id} ${step.description}`);
|
|
174
|
+
} catch (e) {
|
|
175
|
+
step.status = 'failed';
|
|
176
|
+
step.error = (e as Error).message;
|
|
177
|
+
warn(`${step.id} ${step.description} — ${step.error}`);
|
|
178
|
+
if (!opts.continueOnError) break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const count = (s: AgentStep['status']) => plan.steps.filter((x) => x.status === s).length;
|
|
183
|
+
return { done: count('done'), failed: count('failed'), skipped: count('skipped'), steps: plan.steps };
|
|
184
|
+
};
|