@looplia/looplia-cli 0.7.4 → 0.8.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/dist/{chunk-VRBGWKZ6.js → chunk-326UJHZM.js} +1 -1
- package/dist/{chunk-QQGRKUSM.js → chunk-3WLHRD63.js} +3 -3
- package/dist/chunk-JJCGDGRS.js +296 -0
- package/dist/{chunk-XTUQVJYH.js → chunk-MTYPUSCH.js} +21 -22
- package/dist/chunk-QKGHHAFR.js +305 -0
- package/dist/{chunk-PXCY2LDE.js → chunk-RAFHASLI.js} +1261 -617
- package/dist/{claude-agent-sdk-IC25DTKL.js → claude-agent-sdk-FNYGQYFW.js} +5 -2
- package/dist/cli.js +320 -61
- package/dist/{compiler-4B63UTUP-VE77VSJ2.js → compiler-QKB2ZYNK-CYEN6G5G.js} +3 -3
- package/dist/discovery-22DBV6CT.js +241 -0
- package/dist/{dist-LKL7WJ7K.js → dist-HMIWVZMJ.js} +8 -6
- package/dist/{sync-E5PGFGNI-IGGJR7IL.js → sync-XZGFZXZF-6AWT77CE.js} +4 -4
- package/package.json +1 -1
- package/plugins/looplia-core/skills/workflow-executor/SKILL.md +73 -18
- package/dist/chunk-XKLZXCWO.js +0 -1211
- package/plugins/looplia-core/skills/workflow-executor-inline/SKILL.md +0 -217
|
@@ -12,13 +12,13 @@ import {
|
|
|
12
12
|
loadSources,
|
|
13
13
|
removeSource,
|
|
14
14
|
saveSources
|
|
15
|
-
} from "./chunk-
|
|
16
|
-
import "./chunk-
|
|
15
|
+
} from "./chunk-MTYPUSCH.js";
|
|
16
|
+
import "./chunk-326UJHZM.js";
|
|
17
17
|
import {
|
|
18
18
|
init_esm_shims
|
|
19
19
|
} from "./chunk-Y55L47HC.js";
|
|
20
20
|
|
|
21
|
-
// ../../packages/provider/dist/compiler-
|
|
21
|
+
// ../../packages/provider/dist/compiler-QKB2ZYNK.js
|
|
22
22
|
init_esm_shims();
|
|
23
23
|
export {
|
|
24
24
|
DEFAULT_MARKETPLACE_SOURCES,
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import {
|
|
3
|
+
getLoopliaPluginPath
|
|
4
|
+
} from "./chunk-JJCGDGRS.js";
|
|
5
|
+
import {
|
|
6
|
+
isValidPathSegment,
|
|
7
|
+
pathExists
|
|
8
|
+
} from "./chunk-326UJHZM.js";
|
|
9
|
+
import {
|
|
10
|
+
init_esm_shims
|
|
11
|
+
} from "./chunk-Y55L47HC.js";
|
|
12
|
+
|
|
13
|
+
// ../../packages/provider/dist/discovery/index.js
|
|
14
|
+
init_esm_shims();
|
|
15
|
+
import { mkdir, readdir, writeFile } from "fs/promises";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
import { execFile } from "child_process";
|
|
18
|
+
import { promisify } from "util";
|
|
19
|
+
var AUTO_DISCOVERY_PLUGIN_NAME = "auto-discovery-plugin";
|
|
20
|
+
function getAutoDiscoveryPluginPath() {
|
|
21
|
+
return join(getLoopliaPluginPath(), "plugins", AUTO_DISCOVERY_PLUGIN_NAME);
|
|
22
|
+
}
|
|
23
|
+
async function ensureAutoDiscoveryPlugin() {
|
|
24
|
+
const pluginPath = getAutoDiscoveryPluginPath();
|
|
25
|
+
const pluginJsonPath = join(pluginPath, ".claude-plugin", "plugin.json");
|
|
26
|
+
const skillsDir = join(pluginPath, "skills");
|
|
27
|
+
if (await pathExists(join(pluginPath, ".claude-plugin"))) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
await mkdir(join(pluginPath, ".claude-plugin"), { recursive: true });
|
|
31
|
+
await mkdir(skillsDir, { recursive: true });
|
|
32
|
+
const pluginJson = {
|
|
33
|
+
name: "auto-discovery-plugin",
|
|
34
|
+
description: "Auto-discovered skills from skills.sh registry",
|
|
35
|
+
version: "1.0.0",
|
|
36
|
+
author: { name: "Looplia" },
|
|
37
|
+
keywords: ["skills", "auto-discovery", "registry", "skills.sh"],
|
|
38
|
+
homepage: "https://github.com/memorysaver/looplia-core"
|
|
39
|
+
};
|
|
40
|
+
await writeFile(pluginJsonPath, JSON.stringify(pluginJson, null, 2), "utf-8");
|
|
41
|
+
}
|
|
42
|
+
async function installSkillToAutoDiscovery(skillName, skillContent, sourceUrl) {
|
|
43
|
+
if (!isValidPathSegment(skillName)) {
|
|
44
|
+
throw new Error(`Invalid skill name: ${skillName}`);
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
await ensureAutoDiscoveryPlugin();
|
|
48
|
+
const skillDir = join(getAutoDiscoveryPluginPath(), "skills", skillName);
|
|
49
|
+
await mkdir(skillDir, { recursive: true });
|
|
50
|
+
const skillPath = join(skillDir, "SKILL.md");
|
|
51
|
+
await writeFile(skillPath, skillContent, "utf-8");
|
|
52
|
+
if (sourceUrl) {
|
|
53
|
+
const sourcePath = join(skillDir, "source.json");
|
|
54
|
+
await writeFile(
|
|
55
|
+
sourcePath,
|
|
56
|
+
JSON.stringify({ gitUrl: sourceUrl }, null, 2),
|
|
57
|
+
"utf-8"
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
skill: skillName,
|
|
62
|
+
status: "installed",
|
|
63
|
+
path: skillDir
|
|
64
|
+
};
|
|
65
|
+
} catch (error) {
|
|
66
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
67
|
+
return {
|
|
68
|
+
skill: skillName,
|
|
69
|
+
status: "failed",
|
|
70
|
+
error: `Failed to install skill: ${message}`
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function listAutoDiscoveredSkills() {
|
|
75
|
+
const skillsDir = join(getAutoDiscoveryPluginPath(), "skills");
|
|
76
|
+
try {
|
|
77
|
+
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
78
|
+
return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
|
|
79
|
+
} catch {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function isSkillAutoDiscovered(skillName) {
|
|
84
|
+
if (!isValidPathSegment(skillName)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
const skillDir = join(getAutoDiscoveryPluginPath(), "skills", skillName);
|
|
88
|
+
return await pathExists(skillDir);
|
|
89
|
+
}
|
|
90
|
+
var execFileAsync = promisify(execFile);
|
|
91
|
+
var FORMAT1_PATTERN = /^(\S+)\s+-\s+(.+)\s+\(([^)]+)\)$/;
|
|
92
|
+
var FORMAT2_PATTERN = /^([^/]+)\/([^\s]+)\s+-\s+(.+)$/;
|
|
93
|
+
var FORMAT3_PATTERN = /^([^/\s]+)\/([^\s]+)$/;
|
|
94
|
+
var FORMAT4_PATTERN = /^([^/\s]+)\/([^@\s]+)@([^\s]+)$/;
|
|
95
|
+
var SKILL_SUFFIX_PATTERN = /-skill$/;
|
|
96
|
+
var SKILL_PREFIX_PATTERN = /^skill-/;
|
|
97
|
+
var ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
|
|
98
|
+
var GITHUB_SEGMENT_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
99
|
+
async function searchSkills(query) {
|
|
100
|
+
try {
|
|
101
|
+
const { stdout } = await execFileAsync("npx", ["skills", "find", query], {
|
|
102
|
+
timeout: 3e4
|
|
103
|
+
});
|
|
104
|
+
return parseSkillsOutput(stdout);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
107
|
+
console.warn(`Skills search failed: ${message}`);
|
|
108
|
+
console.warn("Continuing with local skills only");
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function deriveSkillName(repo) {
|
|
113
|
+
return repo.replace(SKILL_SUFFIX_PATTERN, "").replace(SKILL_PREFIX_PATTERN, "");
|
|
114
|
+
}
|
|
115
|
+
function tryParseFormat1(line) {
|
|
116
|
+
const match = line.match(FORMAT1_PATTERN);
|
|
117
|
+
if (!match) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const [, name, description, fullRepo] = match;
|
|
121
|
+
if (!(name && description && fullRepo)) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const parts = fullRepo.split("/");
|
|
125
|
+
const owner = parts[0];
|
|
126
|
+
const repo = parts[1];
|
|
127
|
+
if (!(owner && repo)) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
name,
|
|
132
|
+
description,
|
|
133
|
+
owner,
|
|
134
|
+
repo,
|
|
135
|
+
installCommand: `npx skills add ${fullRepo}`
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function tryParseFormat2(line) {
|
|
139
|
+
const match = line.match(FORMAT2_PATTERN);
|
|
140
|
+
if (!match) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
const [, owner, repo, description] = match;
|
|
144
|
+
if (!(owner && repo && description)) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
name: deriveSkillName(repo),
|
|
149
|
+
description,
|
|
150
|
+
owner,
|
|
151
|
+
repo,
|
|
152
|
+
installCommand: `npx skills add ${owner}/${repo}`
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function tryParseFormat3(line) {
|
|
156
|
+
const match = line.match(FORMAT3_PATTERN);
|
|
157
|
+
if (!match) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
const [, owner, repo] = match;
|
|
161
|
+
if (!(owner && repo)) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
name: deriveSkillName(repo),
|
|
166
|
+
description: "",
|
|
167
|
+
owner,
|
|
168
|
+
repo,
|
|
169
|
+
installCommand: `npx skills add ${owner}/${repo}`
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function tryParseFormat4(line) {
|
|
173
|
+
const match = line.match(FORMAT4_PATTERN);
|
|
174
|
+
if (!match) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
const [, owner, repo, skillName] = match;
|
|
178
|
+
if (!(owner && repo && skillName)) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
name: skillName,
|
|
183
|
+
description: "",
|
|
184
|
+
owner,
|
|
185
|
+
repo,
|
|
186
|
+
installCommand: `npx skills add ${owner}/${repo}@${skillName}`
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function isValidGitHubSegment(s) {
|
|
190
|
+
return s.length > 0 && GITHUB_SEGMENT_PATTERN.test(s);
|
|
191
|
+
}
|
|
192
|
+
function stripAnsi(str) {
|
|
193
|
+
return str.replace(ANSI_PATTERN, "");
|
|
194
|
+
}
|
|
195
|
+
function parseSkillsOutput(output) {
|
|
196
|
+
const results = [];
|
|
197
|
+
const lines = output.split("\n").filter((line) => line.trim());
|
|
198
|
+
for (const rawLine of lines) {
|
|
199
|
+
const line = stripAnsi(rawLine);
|
|
200
|
+
const result = tryParseFormat4(line) ?? tryParseFormat1(line) ?? tryParseFormat2(line) ?? tryParseFormat3(line);
|
|
201
|
+
if (result) {
|
|
202
|
+
results.push(result);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return results;
|
|
206
|
+
}
|
|
207
|
+
async function fetchSkillContent(owner, repo, skillName) {
|
|
208
|
+
for (const [label, value] of [
|
|
209
|
+
["owner", owner],
|
|
210
|
+
["repo", repo],
|
|
211
|
+
["skillName", skillName]
|
|
212
|
+
]) {
|
|
213
|
+
if (!isValidGitHubSegment(value)) {
|
|
214
|
+
throw new Error(`Invalid ${label}: ${value}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const paths = [
|
|
218
|
+
`skills/${skillName}/SKILL.md`,
|
|
219
|
+
`${skillName}/SKILL.md`,
|
|
220
|
+
"SKILL.md"
|
|
221
|
+
];
|
|
222
|
+
for (const path of paths) {
|
|
223
|
+
const url = `https://raw.githubusercontent.com/${owner}/${repo}/main/${path}`;
|
|
224
|
+
const response = await fetch(url);
|
|
225
|
+
if (response.ok) {
|
|
226
|
+
return response.text();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
throw new Error(
|
|
230
|
+
`Failed to fetch skill content for ${owner}/${repo}/${skillName}`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
export {
|
|
234
|
+
ensureAutoDiscoveryPlugin,
|
|
235
|
+
fetchSkillContent,
|
|
236
|
+
getAutoDiscoveryPluginPath,
|
|
237
|
+
installSkillToAutoDiscovery,
|
|
238
|
+
isSkillAutoDiscovered,
|
|
239
|
+
listAutoDiscoveredSkills,
|
|
240
|
+
searchSkills
|
|
241
|
+
};
|
|
@@ -11,11 +11,11 @@ import {
|
|
|
11
11
|
loadCompiledRegistry,
|
|
12
12
|
removeSkill,
|
|
13
13
|
updateSkill
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-QKGHHAFR.js";
|
|
15
15
|
import {
|
|
16
16
|
syncRegistrySources,
|
|
17
17
|
syncSource
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-3WLHRD63.js";
|
|
19
19
|
import {
|
|
20
20
|
addSource,
|
|
21
21
|
compileRegistry,
|
|
@@ -27,13 +27,15 @@ import {
|
|
|
27
27
|
loadSources,
|
|
28
28
|
removeSource,
|
|
29
29
|
saveSources
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-MTYPUSCH.js";
|
|
31
|
+
import {
|
|
32
|
+
ensureWorkspace
|
|
33
|
+
} from "./chunk-RAFHASLI.js";
|
|
31
34
|
import {
|
|
32
35
|
CORE_SKILLS,
|
|
33
|
-
ensureWorkspace,
|
|
34
36
|
isCoreSkill
|
|
35
|
-
} from "./chunk-
|
|
36
|
-
import "./chunk-
|
|
37
|
+
} from "./chunk-JJCGDGRS.js";
|
|
38
|
+
import "./chunk-326UJHZM.js";
|
|
37
39
|
import "./chunk-Y55L47HC.js";
|
|
38
40
|
export {
|
|
39
41
|
CORE_SKILLS,
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
import {
|
|
3
3
|
syncRegistrySources,
|
|
4
4
|
syncSource
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
7
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-3WLHRD63.js";
|
|
6
|
+
import "./chunk-MTYPUSCH.js";
|
|
7
|
+
import "./chunk-326UJHZM.js";
|
|
8
8
|
import {
|
|
9
9
|
init_esm_shims
|
|
10
10
|
} from "./chunk-Y55L47HC.js";
|
|
11
11
|
|
|
12
|
-
// ../../packages/provider/dist/sync-
|
|
12
|
+
// ../../packages/provider/dist/sync-XZGFZXZF.js
|
|
13
13
|
init_esm_shims();
|
|
14
14
|
export {
|
|
15
15
|
syncRegistrySources,
|
package/package.json
CHANGED
|
@@ -26,6 +26,29 @@ Use this skill when:
|
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
29
|
+
## ⚠️ CRITICAL EXECUTION MODEL - READ FIRST
|
|
30
|
+
|
|
31
|
+
**YOU (the model reading this skill) must execute the step loop directly. DO NOT delegate.**
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
35
|
+
│ YOU (main agent) iterate through steps, making ONE Task call each │
|
|
36
|
+
│ │
|
|
37
|
+
│ FOR step IN workflow.steps: │
|
|
38
|
+
│ 1. Call Task(general-purpose) for THIS STEP ONLY │
|
|
39
|
+
│ 2. WAIT for Task to complete │
|
|
40
|
+
│ 3. Validate output │
|
|
41
|
+
│ 4. THEN move to next step │
|
|
42
|
+
│ │
|
|
43
|
+
│ ❌ WRONG: Task("Execute all workflow steps...") │
|
|
44
|
+
│ ✅ RIGHT: Task("Execute step: summary"), Task("Execute step: ideas")│
|
|
45
|
+
└─────────────────────────────────────────────────────────────────────┘
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**3 steps = 3 Task calls. You MUST make 3 separate Task tool invocations.**
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
29
52
|
## CRITICAL: Task Invocation with general-purpose Subagent
|
|
30
53
|
|
|
31
54
|
**v0.6.9:** Using built-in `general-purpose` subagent for ALL workflow steps (all providers).
|
|
@@ -76,25 +99,37 @@ Each `Task(general-purpose)` creates a **separate context window**:
|
|
|
76
99
|
|
|
77
100
|
**NEVER batch multiple steps** - this defeats context isolation.
|
|
78
101
|
|
|
79
|
-
### Anti-Patterns
|
|
102
|
+
### Anti-Patterns (VIOLATIONS = TEST FAILURE)
|
|
80
103
|
|
|
81
|
-
❌ **
|
|
104
|
+
❌ **FATAL ERROR - Delegating entire workflow to one Task:**
|
|
82
105
|
```json
|
|
83
106
|
{
|
|
84
107
|
"description": "Execute workflow: writing-kit",
|
|
85
108
|
"prompt": "Run all workflow steps..."
|
|
86
109
|
}
|
|
87
110
|
```
|
|
88
|
-
This
|
|
111
|
+
**This is WRONG.** You made 1 Task call. Tests expect 3. TEST WILL FAIL.
|
|
89
112
|
|
|
90
|
-
|
|
91
|
-
```json
|
|
92
|
-
{
|
|
93
|
-
"description": "Execute step: summary",
|
|
94
|
-
"prompt": "Execute skill 'media-reviewer' for step 'summary'..."
|
|
95
|
-
}
|
|
113
|
+
❌ **FATAL ERROR - Executing skills inline without Task:**
|
|
96
114
|
```
|
|
97
|
-
|
|
115
|
+
Skill("media-reviewer") // NO! You must use Task wrapper
|
|
116
|
+
Skill("idea-synthesis") // NO! You must use Task wrapper
|
|
117
|
+
```
|
|
118
|
+
**This is WRONG.** Each step MUST be wrapped in a Task call.
|
|
119
|
+
|
|
120
|
+
✅ **CORRECT - One Task call per step (3 steps = 3 Task calls):**
|
|
121
|
+
```
|
|
122
|
+
Task("Execute step: summary") // Task #1
|
|
123
|
+
→ Subagent invokes Skill("media-reviewer")
|
|
124
|
+
|
|
125
|
+
Task("Execute step: ideas") // Task #2
|
|
126
|
+
→ Subagent invokes Skill("idea-synthesis")
|
|
127
|
+
|
|
128
|
+
Task("Execute step: writing-kit") // Task #3
|
|
129
|
+
→ Subagent invokes Skill("writing-kit-assembler")
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Verification**: Count your Task tool calls. If workflow has 3 steps, you MUST have 3 Task calls.
|
|
98
133
|
|
|
99
134
|
---
|
|
100
135
|
|
|
@@ -243,18 +278,38 @@ Input:
|
|
|
243
278
|
Computed order: [analyze-content, generate-ideas, build-writing-kit]
|
|
244
279
|
```
|
|
245
280
|
|
|
246
|
-
### Phase 5: Step Execution Loop
|
|
281
|
+
### Phase 5: Step Execution Loop (YOU MUST DO THIS)
|
|
282
|
+
|
|
283
|
+
**YOU execute this loop. DO NOT delegate the loop to a subagent.**
|
|
247
284
|
|
|
248
|
-
|
|
285
|
+
```
|
|
286
|
+
YOU (main agent) must:
|
|
287
|
+
┌────────────────────────────────────────────────────────────────┐
|
|
288
|
+
│ for (const step of steps) { │
|
|
289
|
+
│ // 1. YOU call Task tool │
|
|
290
|
+
│ await Task({ │
|
|
291
|
+
│ subagent_type: "general-purpose", │
|
|
292
|
+
│ description: `Execute step: ${step.id}`, │
|
|
293
|
+
│ prompt: `Execute skill '${step.skill}' for step...` │
|
|
294
|
+
│ }); │
|
|
295
|
+
│ │
|
|
296
|
+
│ // 2. YOU wait for Task to complete │
|
|
297
|
+
│ // 3. YOU validate output │
|
|
298
|
+
│ // 4. YOU update validation.json │
|
|
299
|
+
│ // 5. YOU move to next step │
|
|
300
|
+
│ } │
|
|
301
|
+
└────────────────────────────────────────────────────────────────┘
|
|
302
|
+
```
|
|
249
303
|
|
|
304
|
+
**Execution checklist:**
|
|
250
305
|
1. Get first unvalidated step from dependency order
|
|
251
|
-
2.
|
|
252
|
-
3. WAIT for Task completion before proceeding
|
|
253
|
-
4.
|
|
254
|
-
5. REPEAT for next unvalidated step
|
|
255
|
-
6. STOP when ALL steps are validated
|
|
306
|
+
2. **YOU** make ONE `Task(general-purpose)` call for THIS step only
|
|
307
|
+
3. **YOU** WAIT for Task completion before proceeding
|
|
308
|
+
4. **YOU** validate output, update validation.json
|
|
309
|
+
5. **YOU** REPEAT for next unvalidated step
|
|
310
|
+
6. **YOU** STOP when ALL steps are validated
|
|
256
311
|
|
|
257
|
-
**MANDATORY:**
|
|
312
|
+
**MANDATORY:** 3 steps = 3 Task calls. Count them. If you made fewer, you did it wrong.
|
|
258
313
|
|
|
259
314
|
```
|
|
260
315
|
FOR EACH step in dependency order:
|