@gregiteen/ssss-cli 0.7.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/CHANGELOG.md +124 -0
- package/LICENSE +15 -0
- package/README.md +162 -0
- package/VERSION +1 -0
- package/conformance/README.md +72 -0
- package/conformance/fixtures.json +763 -0
- package/conformance/reference-bundle.ucw.json +135 -0
- package/docs/help/autolink.md +15 -0
- package/docs/help/bundle.md +47 -0
- package/docs/help/conformance.md +34 -0
- package/docs/help/export.md +20 -0
- package/docs/help/import.md +19 -0
- package/docs/help/overview.md +33 -0
- package/docs/help/portability.md +27 -0
- package/docs/help/provision.md +19 -0
- package/docs/help/provisioning.md +43 -0
- package/docs/help/roles.md +61 -0
- package/docs/help/runtime.md +78 -0
- package/docs/help/scaffold.md +47 -0
- package/docs/ssss-spec.md +1325 -0
- package/package.json +62 -0
- package/registry/core.json +225 -0
- package/registry/extensions/festech.json +134 -0
- package/scripts/audit-registry-field-usage.mjs +136 -0
- package/scripts/autolink.mjs +341 -0
- package/scripts/build-reference-bundle.mjs +169 -0
- package/scripts/cmd-export.mjs +72 -0
- package/scripts/cmd-help.mjs +60 -0
- package/scripts/cmd-import.mjs +71 -0
- package/scripts/cmd-inspect.mjs +78 -0
- package/scripts/cmd-new.mjs +262 -0
- package/scripts/cmd-provision.mjs +68 -0
- package/scripts/cmd-validate.mjs +43 -0
- package/scripts/conformance.mjs +408 -0
- package/scripts/lib/cli.mjs +92 -0
- package/scripts/release.sh +35 -0
- package/scripts/ssss.mjs +93 -0
- package/scripts/validate-skills.mjs +115 -0
- package/src/bundle.mjs +261 -0
- package/src/engine.mjs +452 -0
- package/src/frontmatter.mjs +109 -0
- package/src/registry.mjs +67 -0
- package/src/runtime.mjs +212 -0
package/src/runtime.mjs
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSSS workflow runtime helpers.
|
|
3
|
+
*
|
|
4
|
+
* This is not a resident daemon. It is the deterministic planning layer a daemon,
|
|
5
|
+
* cron bridge, webhook worker, or agent host uses to turn a workflow trigger into
|
|
6
|
+
* Operation Contract envelopes. The vault remains the source of truth; scheduler
|
|
7
|
+
* queues and dashboards are projections.
|
|
8
|
+
*/
|
|
9
|
+
import crypto from 'node:crypto';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { parseDocument } from './frontmatter.mjs';
|
|
12
|
+
|
|
13
|
+
export const RUNTIME_TRIGGER_TYPES = ['manual', 'cron', 'interval', 'event', 'webhook', 'file_change', 'condition'];
|
|
14
|
+
export const RUNTIME_MISFIRE_POLICIES = ['skip', 'run_once', 'catch_up'];
|
|
15
|
+
export const RUNTIME_CONCURRENCY_POLICIES = ['allow_parallel', 'skip_if_running', 'enqueue', 'replace'];
|
|
16
|
+
export const RUN_STATUSES = ['queued', 'claimed', 'running', 'waiting_for_human', 'succeeded', 'failed', 'canceled', 'retrying', 'dead_letter'];
|
|
17
|
+
|
|
18
|
+
function hash(input, length = 32) {
|
|
19
|
+
return crypto.createHash('sha256').update(String(input)).digest('hex').slice(0, length);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function slugify(input) {
|
|
23
|
+
return String(input || '')
|
|
24
|
+
.trim()
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
27
|
+
.replace(/^-+|-+$/g, '') || 'workflow';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function yamlString(value) {
|
|
31
|
+
return JSON.stringify(String(value));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function iso(value, field) {
|
|
35
|
+
if (value instanceof Date) return value.toISOString();
|
|
36
|
+
const d = new Date(value);
|
|
37
|
+
if (Number.isNaN(d.getTime())) throw new Error(`${field} must be an ISO-parseable timestamp.`);
|
|
38
|
+
return d.toISOString();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function compactTimestamp(value) {
|
|
42
|
+
return value.replace(/[^0-9TZ]/g, '').replace(/T.*Z$/, (s) => s.replace(/[^0-9TZ]/g, ''));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function workflowIdFromPath(workflowPath, data = {}) {
|
|
46
|
+
if (data.workflow_id) return slugify(data.workflow_id);
|
|
47
|
+
if (data.slug) return slugify(data.slug);
|
|
48
|
+
const base = path.basename(workflowPath || '', '.md');
|
|
49
|
+
if (base && base !== 'WORKFLOW') return slugify(base);
|
|
50
|
+
const parent = path.basename(path.dirname(workflowPath || ''));
|
|
51
|
+
return slugify(parent || data.name || 'workflow');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function normalizeRuntimeTrigger(trigger = {}) {
|
|
55
|
+
if (!trigger || typeof trigger !== 'object') throw new Error('Runtime trigger must be an object.');
|
|
56
|
+
const type = trigger.type || 'manual';
|
|
57
|
+
if (!RUNTIME_TRIGGER_TYPES.includes(type)) {
|
|
58
|
+
throw new Error(`Unknown runtime trigger type '${type}'. Must be one of: ${RUNTIME_TRIGGER_TYPES.join(', ')}.`);
|
|
59
|
+
}
|
|
60
|
+
const discriminator =
|
|
61
|
+
trigger.trigger_id || trigger.id || trigger.name || trigger.cron || trigger.interval ||
|
|
62
|
+
trigger.event_type || trigger.webhook_id || trigger.path || trigger.condition || type;
|
|
63
|
+
return {
|
|
64
|
+
...trigger,
|
|
65
|
+
type,
|
|
66
|
+
trigger_id: slugify(discriminator),
|
|
67
|
+
misfire_policy: trigger.misfire_policy || 'run_once',
|
|
68
|
+
concurrency: trigger.concurrency || 'skip_if_running',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function planWorkflowTrigger({
|
|
73
|
+
workflowPath,
|
|
74
|
+
workflowContent,
|
|
75
|
+
workspaceId,
|
|
76
|
+
trigger = { type: 'manual' },
|
|
77
|
+
scheduledFor,
|
|
78
|
+
firedAt = scheduledFor || new Date().toISOString(),
|
|
79
|
+
payload = {},
|
|
80
|
+
actor = { role: 'system' },
|
|
81
|
+
} = {}) {
|
|
82
|
+
if (!workflowPath) throw new Error('workflowPath is required.');
|
|
83
|
+
if (!workflowContent) throw new Error('workflowContent is required.');
|
|
84
|
+
if (!workspaceId) throw new Error('workspaceId is required.');
|
|
85
|
+
|
|
86
|
+
const { data, body } = parseDocument(workflowContent);
|
|
87
|
+
if (data.type !== 'workflow') throw new Error(`Expected type: workflow at ${workflowPath}.`);
|
|
88
|
+
if (!data.name) throw new Error(`Workflow ${workflowPath} is missing required field 'name'.`);
|
|
89
|
+
|
|
90
|
+
const normalizedTrigger = normalizeRuntimeTrigger(trigger);
|
|
91
|
+
const workflow_id = workflowIdFromPath(workflowPath, data);
|
|
92
|
+
const scheduled_for = iso(scheduledFor || firedAt, 'scheduledFor');
|
|
93
|
+
const fired_at = iso(firedAt || scheduled_for, 'firedAt');
|
|
94
|
+
const basis = `${workspaceId}:${workflow_id}:${normalizedTrigger.trigger_id}:${scheduled_for}`;
|
|
95
|
+
const dedupe_key = `runtime:${hash(basis, 40)}`;
|
|
96
|
+
const stamp = compactTimestamp(scheduled_for);
|
|
97
|
+
const taskPath = `tasks/${workflow_id}/${normalizedTrigger.trigger_id}-${stamp}.md`;
|
|
98
|
+
const eventPath = `events/workflows/${workflow_id}/${normalizedTrigger.trigger_id}-${stamp}`;
|
|
99
|
+
|
|
100
|
+
const eventPayload = {
|
|
101
|
+
event_type: 'workflow_triggered',
|
|
102
|
+
workflow_id,
|
|
103
|
+
workflow_path: workflowPath,
|
|
104
|
+
trigger_id: normalizedTrigger.trigger_id,
|
|
105
|
+
trigger_type: normalizedTrigger.type,
|
|
106
|
+
scheduled_for,
|
|
107
|
+
fired_at,
|
|
108
|
+
dedupe_key,
|
|
109
|
+
payload,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const taskContent = [
|
|
113
|
+
'---',
|
|
114
|
+
'type: task',
|
|
115
|
+
`title: ${yamlString(`Task: ${data.name}`)}`,
|
|
116
|
+
`description: ${yamlString(`Instantiated from ${workflowPath} by ${normalizedTrigger.type} trigger ${normalizedTrigger.trigger_id}.`)}`,
|
|
117
|
+
`timestamp: ${yamlString(fired_at)}`,
|
|
118
|
+
`priority: ${Number.isInteger(data.priority) ? data.priority : 50}`,
|
|
119
|
+
'category: workflow',
|
|
120
|
+
'status: pending',
|
|
121
|
+
`workflow_id: ${yamlString(workflow_id)}`,
|
|
122
|
+
`workflow_path: ${yamlString(workflowPath)}`,
|
|
123
|
+
`trigger_id: ${yamlString(normalizedTrigger.trigger_id)}`,
|
|
124
|
+
`trigger_type: ${yamlString(normalizedTrigger.type)}`,
|
|
125
|
+
`scheduled_for: ${yamlString(scheduled_for)}`,
|
|
126
|
+
`dedupe_key: ${yamlString(dedupe_key)}`,
|
|
127
|
+
'---',
|
|
128
|
+
'',
|
|
129
|
+
`# Task: ${data.name}`,
|
|
130
|
+
'',
|
|
131
|
+
`Instantiated from ${workflowPath} by ${normalizedTrigger.type} trigger ${normalizedTrigger.trigger_id}.`,
|
|
132
|
+
'',
|
|
133
|
+
'## Workflow Steps',
|
|
134
|
+
'',
|
|
135
|
+
body.trim() || '_No workflow body provided._',
|
|
136
|
+
'',
|
|
137
|
+
].join('\n');
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
workflow_id,
|
|
141
|
+
trigger_id: normalizedTrigger.trigger_id,
|
|
142
|
+
scheduled_for,
|
|
143
|
+
dedupe_key,
|
|
144
|
+
envelopes: [
|
|
145
|
+
{
|
|
146
|
+
type: 'event',
|
|
147
|
+
idempotency_key: `runtime-event-${hash(`event:${basis}`, 32)}`,
|
|
148
|
+
path: eventPath,
|
|
149
|
+
workspace_id: workspaceId,
|
|
150
|
+
content: JSON.stringify(eventPayload),
|
|
151
|
+
intent: 'Runtime trigger event',
|
|
152
|
+
actor,
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
type: 'operation',
|
|
156
|
+
idempotency_key: `runtime-task-${hash(`task:${basis}`, 32)}`,
|
|
157
|
+
path: taskPath,
|
|
158
|
+
workspace_id: workspaceId,
|
|
159
|
+
content: taskContent,
|
|
160
|
+
intent: 'Runtime task instantiated from workflow trigger',
|
|
161
|
+
actor,
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function createRunEnvelope({
|
|
168
|
+
workflowId,
|
|
169
|
+
workspaceId,
|
|
170
|
+
taskPath,
|
|
171
|
+
runId,
|
|
172
|
+
taskId = taskPath,
|
|
173
|
+
status = 'queued',
|
|
174
|
+
startedAt = new Date().toISOString(),
|
|
175
|
+
actor = { role: 'system' },
|
|
176
|
+
} = {}) {
|
|
177
|
+
if (!workflowId) throw new Error('workflowId is required.');
|
|
178
|
+
if (!workspaceId) throw new Error('workspaceId is required.');
|
|
179
|
+
if (!taskPath) throw new Error('taskPath is required.');
|
|
180
|
+
if (!RUN_STATUSES.includes(status)) {
|
|
181
|
+
throw new Error(`Unknown run status '${status}'. Must be one of: ${RUN_STATUSES.join(', ')}.`);
|
|
182
|
+
}
|
|
183
|
+
const run_id = runId || `run-${hash(`${workspaceId}:${workflowId}:${taskPath}`, 24)}`;
|
|
184
|
+
const content = [
|
|
185
|
+
'---',
|
|
186
|
+
'type: run',
|
|
187
|
+
`title: ${yamlString(`Run ${run_id}`)}`,
|
|
188
|
+
`description: ${yamlString(`Workflow run for task ${taskPath}.`)}`,
|
|
189
|
+
`timestamp: ${yamlString(iso(startedAt, 'startedAt'))}`,
|
|
190
|
+
`run_id: ${yamlString(run_id)}`,
|
|
191
|
+
`workflow_id: ${yamlString(workflowId)}`,
|
|
192
|
+
`task_id: ${yamlString(taskId)}`,
|
|
193
|
+
`task_path: ${yamlString(taskPath)}`,
|
|
194
|
+
`workspace_id: ${yamlString(workspaceId)}`,
|
|
195
|
+
`status: ${status}`,
|
|
196
|
+
`started_at: ${yamlString(iso(startedAt, 'startedAt'))}`,
|
|
197
|
+
'---',
|
|
198
|
+
'',
|
|
199
|
+
`### run created - ${iso(startedAt, 'startedAt')} - ${status}`,
|
|
200
|
+
`Task: ${taskPath}`,
|
|
201
|
+
'',
|
|
202
|
+
].join('\n');
|
|
203
|
+
return {
|
|
204
|
+
type: 'operation',
|
|
205
|
+
idempotency_key: `runtime-run-${hash(`${workspaceId}:${workflowId}:${taskPath}:${run_id}`, 32)}`,
|
|
206
|
+
path: `runs/${workflowId}/${run_id}/RUN.md`,
|
|
207
|
+
workspace_id: workspaceId,
|
|
208
|
+
content,
|
|
209
|
+
intent: 'Runtime run created for claimed task',
|
|
210
|
+
actor,
|
|
211
|
+
};
|
|
212
|
+
}
|