@worca/app 0.0.1
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/README.md +403 -0
- package/agents/clarify.meta.json +19 -0
- package/agents/decomposer.meta.json +21 -0
- package/agents/implementer.meta.json +20 -0
- package/agents/manualTestsChecklist.meta.json +18 -0
- package/agents/manualWebUiTesting.meta.json +18 -0
- package/agents/planReviewer.meta.json +19 -0
- package/agents/planner.meta.json +20 -0
- package/agents/refiner.meta.json +19 -0
- package/agents/reviewer.meta.json +19 -0
- package/agents/worca-cc-clarify.md +67 -0
- package/agents/worca-cc-code-reviewer.md +66 -0
- package/agents/worca-cc-decomposer.md +84 -0
- package/agents/worca-cc-implementer.md +69 -0
- package/agents/worca-cc-manual-tests-checklist.md +63 -0
- package/agents/worca-cc-manual-web-ui-testing.md +64 -0
- package/agents/worca-cc-plan-refiner.md +69 -0
- package/agents/worca-cc-plan-reviewer.md +70 -0
- package/agents/worca-cc-planner.md +70 -0
- package/agents/worca-cc-workspace-reviewer.md +56 -0
- package/agents/worca-cc-workspace-scanner.md +55 -0
- package/agents/workspaceReviewer.meta.json +20 -0
- package/agents/workspaceScanner.meta.json +18 -0
- package/package.json +61 -0
- package/scripts/install.mjs +209 -0
- package/skills/worca/SKILL.md +66 -0
- package/src/cli/worca-cc.mjs +1520 -0
- package/src/core/agent-gen.mjs +206 -0
- package/src/core/agent-registry.mjs +417 -0
- package/src/core/agent-store.mjs +143 -0
- package/src/core/artifacts.mjs +2019 -0
- package/src/core/channels.mjs +302 -0
- package/src/core/chat/allowlist.mjs +27 -0
- package/src/core/chat/channel-host.mjs +562 -0
- package/src/core/chat/channel-protocol.mjs +117 -0
- package/src/core/chat/channel-worker-child.mjs +211 -0
- package/src/core/chat/chat-context.mjs +66 -0
- package/src/core/chat/command-router.mjs +343 -0
- package/src/core/chat/notifier.mjs +120 -0
- package/src/core/chat/parser.mjs +30 -0
- package/src/core/chat/rate-limiter.mjs +133 -0
- package/src/core/chat/redact.mjs +27 -0
- package/src/core/chat/renderers.mjs +136 -0
- package/src/core/claude-runner.mjs +1356 -0
- package/src/core/config.mjs +882 -0
- package/src/core/cost-budget.mjs +103 -0
- package/src/core/db.mjs +864 -0
- package/src/core/fanout.mjs +48 -0
- package/src/core/folder-dialog.mjs +138 -0
- package/src/core/fs-browse.mjs +49 -0
- package/src/core/git-info.mjs +200 -0
- package/src/core/guardrail-store.mjs +204 -0
- package/src/core/guardrails.mjs +302 -0
- package/src/core/marketplaces.mjs +267 -0
- package/src/core/migrate-fs-to-db.mjs +612 -0
- package/src/core/model-env.mjs +74 -0
- package/src/core/orchestrator.mjs +4279 -0
- package/src/core/overview-agent.mjs +124 -0
- package/src/core/phases.mjs +1279 -0
- package/src/core/pipeline-delete.mjs +428 -0
- package/src/core/plugin-api.mjs +13 -0
- package/src/core/plugin-config.mjs +100 -0
- package/src/core/plugin-inventory.mjs +50 -0
- package/src/core/plugin-manifest.mjs +447 -0
- package/src/core/plugin-models.mjs +130 -0
- package/src/core/plugin-repo.mjs +303 -0
- package/src/core/plugin-shim-child.mjs +76 -0
- package/src/core/plugin-shim.mjs +197 -0
- package/src/core/plugin-store.mjs +485 -0
- package/src/core/plugin-workflows.mjs +179 -0
- package/src/core/plugins-lock.mjs +49 -0
- package/src/core/preflight-node.mjs +122 -0
- package/src/core/preflight.mjs +341 -0
- package/src/core/projects.mjs +157 -0
- package/src/core/protocol.mjs +257 -0
- package/src/core/recoverable-error.mjs +51 -0
- package/src/core/results.mjs +188 -0
- package/src/core/run-context.mjs +1375 -0
- package/src/core/run-log.mjs +64 -0
- package/src/core/run-manifest.mjs +317 -0
- package/src/core/runners.mjs +167 -0
- package/src/core/settings.mjs +682 -0
- package/src/core/skills.mjs +210 -0
- package/src/core/sources.mjs +232 -0
- package/src/core/stats.mjs +182 -0
- package/src/core/store.mjs +67 -0
- package/src/core/title.mjs +64 -0
- package/src/core/workflow-validator.mjs +185 -0
- package/src/core/workflows.mjs +568 -0
- package/src/core/workspace-scan.mjs +420 -0
- package/src/core/workspaces.mjs +353 -0
- package/src/core/worktree.mjs +708 -0
- package/src/feature.mjs +9 -0
- package/ui/public/app.js +10647 -0
- package/ui/public/assets/worca-favicon.png +0 -0
- package/ui/public/assets/worca-logo.png +0 -0
- package/ui/public/chat-settings-view.mjs +89 -0
- package/ui/public/composer-core.mjs +211 -0
- package/ui/public/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/ui/public/fonts/poppins-latin-400-normal.woff2 +0 -0
- package/ui/public/fonts/poppins-latin-500-normal.woff2 +0 -0
- package/ui/public/fonts/poppins-latin-600-normal.woff2 +0 -0
- package/ui/public/fonts/poppins-latin-700-normal.woff2 +0 -0
- package/ui/public/guardrails-view.mjs +244 -0
- package/ui/public/index.html +1145 -0
- package/ui/public/log-filter.mjs +81 -0
- package/ui/public/log-line.mjs +86 -0
- package/ui/public/models-view.mjs +433 -0
- package/ui/public/plugins-view.mjs +430 -0
- package/ui/public/results-view.mjs +121 -0
- package/ui/public/source-pane.mjs +156 -0
- package/ui/public/stats-view.mjs +523 -0
- package/ui/public/style.css +1557 -0
- package/ui/server.mjs +3573 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// src/core/channels.mjs
|
|
2
|
+
// Typed channel vocabulary + the dispatch-time binder. Pure: the only IO is the
|
|
3
|
+
// path STRINGS it computes via artifacts.mjs (no reads/writes). Replaces the
|
|
4
|
+
// role-coupled _nodeIo/_afterNode switch with data-driven channel wiring.
|
|
5
|
+
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { planPath, reviewPath } from './artifacts.mjs';
|
|
8
|
+
|
|
9
|
+
/** The closed set of channel ids (single source; imported by registry + validator).
|
|
10
|
+
* `workspace` (M3) is a read-only metadata channel carrying the frozen workspace
|
|
11
|
+
* description + member set; it is seeded once and never re-published (CONV-6). */
|
|
12
|
+
export const CHANNEL_IDS = ['userPrompt', 'plan', 'review', 'checklist', 'code', 'workspace', 'clarify', 'decomposition'];
|
|
13
|
+
|
|
14
|
+
/** Channels the dispatcher pre-seeds WITH VALUES on the bus at run start
|
|
15
|
+
* (orchestrator.mjs _dispatch bus literal): userPrompt IS the prompt,
|
|
16
|
+
* plan/checklist get default destinations, code is the standing worktree.
|
|
17
|
+
* The bus literal also carries workspace (metadata or null) and clarify/
|
|
18
|
+
* decomposition seeded as null — those are deliberately NOT listed here so the
|
|
19
|
+
* validator keeps warning when they are consumed with no upstream producer
|
|
20
|
+
* (current behavior). Single source for the validator's reachability pass —
|
|
21
|
+
* keep in sync with the _dispatch bus literal. */
|
|
22
|
+
export const PRESEEDED_CHANNELS = ['userPrompt', 'plan', 'checklist', 'code'];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Mint the concrete OUTPUT handle for a channel the node produces. This is the
|
|
26
|
+
* relocated output half of the old _nodeIo switch; paths are byte-identical so
|
|
27
|
+
* history/state.json/MOCK markers do not move.
|
|
28
|
+
* @param {string} channel
|
|
29
|
+
* @param {{projectDir,pipelineDir,baseName,datePrefix,cycle,key?}} ctx
|
|
30
|
+
*/
|
|
31
|
+
export function allocate(channel, ctx) {
|
|
32
|
+
const { projectDir, pipelineDir, baseName, datePrefix, cycle, key, workspaceKey } = ctx;
|
|
33
|
+
switch (channel) {
|
|
34
|
+
case 'plan': {
|
|
35
|
+
// The planner is cycle-aware so a plan-review -> planner loop writes a fresh
|
|
36
|
+
// -vN on each replan instead of clobbering v1 (cycle is 1 on the first pass,
|
|
37
|
+
// so v1 has NO -v suffix, matching today's seeded io.planPath). The refiner
|
|
38
|
+
// versions up to cycle+1 (a self-loop never produces v1). workspaceKey (when
|
|
39
|
+
// present) routes the unified plan to the workspace store; absent it is byte-
|
|
40
|
+
// identical to the single-project path.
|
|
41
|
+
const version = key === 'planner' ? cycle : cycle + 1;
|
|
42
|
+
return { kind: 'artifact', path: planPath(projectDir, baseName, version, datePrefix, workspaceKey) };
|
|
43
|
+
}
|
|
44
|
+
case 'review': {
|
|
45
|
+
// Review json basename differs by producing role (keeps existing filenames).
|
|
46
|
+
// A CUSTOM verifier key gets its own `<key>-review` basename so it can never
|
|
47
|
+
// clobber the built-in reviewer's impl-review files.
|
|
48
|
+
const k = key || 'reviewer';
|
|
49
|
+
const BESPOKE_BASE = {
|
|
50
|
+
reviewer: 'impl-review',
|
|
51
|
+
refiner: 'refine-review',
|
|
52
|
+
manualWebUiTesting: 'webui-review',
|
|
53
|
+
planReviewer: 'plan-review',
|
|
54
|
+
workspaceReviewer: 'ws-review',
|
|
55
|
+
};
|
|
56
|
+
let base = BESPOKE_BASE[k] || `${k}-review`;
|
|
57
|
+
// A custom key like "impl" would mint impl-review-* and clobber the built-in
|
|
58
|
+
// reviewer's files AND its DB verdict (reviewKind upsert); divert it.
|
|
59
|
+
if (!BESPOKE_BASE[k] && Object.values(BESPOKE_BASE).includes(base)) base = `${k}-agent-review`;
|
|
60
|
+
// ▲ C2: the refiner emits ONLY a json verdict (its md is null => private to its
|
|
61
|
+
// self-loop). Every other verifier carries a review md so publish() folds it
|
|
62
|
+
// onto the shared `review` channel its loop target consumes. workspaceKey routes
|
|
63
|
+
// the review md to the workspace store (the json stays per-cycle in the pipeline
|
|
64
|
+
// dir, store-root independent); absent it is byte-identical. Custom verifiers
|
|
65
|
+
// keep their md pipeline-dir-local (like webui-review).
|
|
66
|
+
const mdPath = k === 'refiner'
|
|
67
|
+
? null
|
|
68
|
+
: k === 'manualWebUiTesting'
|
|
69
|
+
? join(pipelineDir, `webui-review-cycle${cycle}.md`)
|
|
70
|
+
: k === 'planReviewer'
|
|
71
|
+
? reviewPath(projectDir, baseName, datePrefix, 'plan-review', workspaceKey)
|
|
72
|
+
: k === 'workspaceReviewer'
|
|
73
|
+
? reviewPath(projectDir, baseName, datePrefix, 'ws-review', workspaceKey)
|
|
74
|
+
: k === 'reviewer'
|
|
75
|
+
? reviewPath(projectDir, baseName, datePrefix, 'impl-review', workspaceKey)
|
|
76
|
+
: join(pipelineDir, `${base}-cycle${cycle}.md`);
|
|
77
|
+
return { kind: 'review', mdPath, jsonPath: join(pipelineDir, `${base}-cycle${cycle}.json`), reviewKind: base };
|
|
78
|
+
}
|
|
79
|
+
case 'checklist':
|
|
80
|
+
return { kind: 'artifact', path: join(pipelineDir, 'manual-tests-checklist.md') };
|
|
81
|
+
case 'code':
|
|
82
|
+
return { kind: 'worktree' }; // standing channel; nothing to allocate
|
|
83
|
+
case 'workspace':
|
|
84
|
+
// Read-only metadata handle: the frozen workspace description snapshot. The
|
|
85
|
+
// member set + description are seeded onto the bus directly by the orchestrator
|
|
86
|
+
// (this just names the on-disk path of the frozen snapshot).
|
|
87
|
+
return { kind: 'metadata', path: join(pipelineDir, 'workspace-description.md') };
|
|
88
|
+
case 'decomposition':
|
|
89
|
+
// The decomposer writes decomposition.json (phase->task index) into the run dir,
|
|
90
|
+
// alongside a tasks/ subdir of self-contained task markdown files.
|
|
91
|
+
return { kind: 'artifact', path: join(pipelineDir, 'decomposition.json') };
|
|
92
|
+
case 'clarify':
|
|
93
|
+
// The clarify agent writes clarify.json into the pipeline dir as scratch; the
|
|
94
|
+
// DB clarify row is authoritative. Same path the legacy pre-step used.
|
|
95
|
+
return { kind: 'clarify', path: join(pipelineDir, 'clarify.json') };
|
|
96
|
+
default: {
|
|
97
|
+
// Open vocabulary: any custom channel allocates a generic pipeline-dir
|
|
98
|
+
// artifact. A registry channelDef (threaded via ctx.channelDefs) overrides
|
|
99
|
+
// kind (md|json) and filename; absent a def the channel defaults to
|
|
100
|
+
// markdown <id>.md. Loop re-runs suffix -cycle<N> so a rewind never
|
|
101
|
+
// clobbers an earlier cycle's output.
|
|
102
|
+
const def = (ctx.channelDefs && ctx.channelDefs[channel]) || null;
|
|
103
|
+
const ext = def?.kind === 'json' ? 'json' : 'md';
|
|
104
|
+
const stem = String(def?.filename || `${channel}.${ext}`).replace(/\.(md|json)$/i, '');
|
|
105
|
+
const name = Number(cycle) > 1 ? `${stem}-cycle${cycle}.${ext}` : `${stem}.${ext}`;
|
|
106
|
+
return { kind: 'artifact', path: join(pipelineDir, name), channel };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Resolve a node's inputs from the bus (latest-writer-wins). Required channels that
|
|
113
|
+
* are null still pass through (the agent/mock proceeds, e.g. a pre-seeded but
|
|
114
|
+
* unwritten plan path). Optional channels that are null are omitted entirely.
|
|
115
|
+
*/
|
|
116
|
+
export function bindInputs(consumes, optionalConsumes, bus) {
|
|
117
|
+
const optional = new Set(optionalConsumes || []);
|
|
118
|
+
const inputs = {};
|
|
119
|
+
for (const c of consumes || []) {
|
|
120
|
+
const v = bus[c];
|
|
121
|
+
if (v == null && optional.has(c)) continue; // omit absent optional channel
|
|
122
|
+
inputs[c] = v;
|
|
123
|
+
}
|
|
124
|
+
return inputs;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Fold a node's result back onto the bus for each produced channel, in node order.
|
|
129
|
+
* Clearing `review` on a `code` publish fixes the sticky fix-mode bug: a later
|
|
130
|
+
* implementer no longer sees a stale review.
|
|
131
|
+
*/
|
|
132
|
+
export function publish(produces, result, outputs, bus) {
|
|
133
|
+
if (!result) return;
|
|
134
|
+
for (const c of produces || []) {
|
|
135
|
+
if (c === 'plan') {
|
|
136
|
+
const path = result.outPlanPath || result.planPath || outputs.plan?.path;
|
|
137
|
+
if (path) bus.plan = { kind: 'artifact', path };
|
|
138
|
+
} else if (c === 'review') {
|
|
139
|
+
// ▲ C2: only a review carrying an md (reviewer / manualWebUiTesting) is shared.
|
|
140
|
+
// The refiner's md-less verdict stays private to its self-loop (read from the
|
|
141
|
+
// results array by _reviewOf, never the bus), so it can't flip a later
|
|
142
|
+
// implementer into `fix` mode.
|
|
143
|
+
const mdPath = result.reviewMdPath ?? outputs.review?.mdPath;
|
|
144
|
+
if (!mdPath) continue;
|
|
145
|
+
bus.review = { kind: 'review', mdPath, jsonPath: outputs.review?.jsonPath, verdict: result.review, reviewKind: outputs.review?.reviewKind };
|
|
146
|
+
} else if (c === 'checklist') {
|
|
147
|
+
const path = result.checklistPath || outputs.checklist?.path;
|
|
148
|
+
if (path) bus.checklist = { kind: 'artifact', path };
|
|
149
|
+
} else if (c === 'code') {
|
|
150
|
+
bus.review = null; // implementer superseded any pending review (fix-mode reset)
|
|
151
|
+
} else if (c === 'workspace') {
|
|
152
|
+
// Read-only metadata: seeded once by the orchestrator, NEVER re-published
|
|
153
|
+
// (CONV-6: the frozen per-step snapshot must not change mid-run). No-op.
|
|
154
|
+
} else if (c === 'clarify') {
|
|
155
|
+
// The clarify node carries questions (from the agent) + answers (from the
|
|
156
|
+
// interactive gate) in its result; fold both onto the bus so the planner reads
|
|
157
|
+
// inputs.clarify.answers.
|
|
158
|
+
bus.clarify = {
|
|
159
|
+
kind: 'clarify',
|
|
160
|
+
path: outputs.clarify?.path,
|
|
161
|
+
questions: result.questions || [],
|
|
162
|
+
answers: result.answers || [],
|
|
163
|
+
};
|
|
164
|
+
} else if (c === 'decomposition') {
|
|
165
|
+
// Fold the decomposer's parsed phases onto the bus so _runStep's implement
|
|
166
|
+
// step can fan out one implementer per task.
|
|
167
|
+
const path = result.decompositionPath || outputs.decomposition?.path;
|
|
168
|
+
bus.decomposition = {
|
|
169
|
+
kind: 'artifact',
|
|
170
|
+
path: path || null,
|
|
171
|
+
phases: Array.isArray(result.decomposition?.phases) ? result.decomposition.phases : [],
|
|
172
|
+
};
|
|
173
|
+
} else {
|
|
174
|
+
// Open vocabulary: a custom channel folds its allocated artifact handle onto
|
|
175
|
+
// the bus (latest-writer-wins), mirroring plan/checklist.
|
|
176
|
+
const path = outputs?.[c]?.path;
|
|
177
|
+
if (path) bus[c] = { kind: 'artifact', path };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Adapter: flatten typed inputs/outputs into the EXACT field names the phases.mjs
|
|
184
|
+
* runners already read (verified against runners.mjs), so runners.mjs/phases.mjs and
|
|
185
|
+
* the runner/dispatcher tests need ZERO changes. This is one of two role switches
|
|
186
|
+
* (the other is runners.mjs's producer/verifier); it names fields, it does not
|
|
187
|
+
* compute data flow.
|
|
188
|
+
*/
|
|
189
|
+
export function legacyFields(node, inputs, outputs, cycle, baseName) {
|
|
190
|
+
const fields = legacyRoleFields(node, inputs, outputs, cycle, baseName);
|
|
191
|
+
// Workspace runs: surface the read-only metadata channel (description + member
|
|
192
|
+
// set) onto the flattened ctx so phases.mjs runners read ctx.workspace. Absent
|
|
193
|
+
// the channel (every single-project node) the field is never added, so the ctx
|
|
194
|
+
// shape stays byte-identical.
|
|
195
|
+
if (inputs && inputs.workspace) fields.workspace = inputs.workspace;
|
|
196
|
+
return fields;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Per-role field naming (the original legacyFields body). */
|
|
200
|
+
function legacyRoleFields(node, inputs, outputs, cycle, baseName) {
|
|
201
|
+
switch (node.key) {
|
|
202
|
+
case 'planner':
|
|
203
|
+
// reviewPath is set only when a review is bound (a plan-review -> planner
|
|
204
|
+
// rewind), which switches runPlannerPlan into its cold-replan branch.
|
|
205
|
+
return { planFilePath: outputs.plan?.path, baseName, answers: inputs.clarify?.answers || inputs.userPrompt?.answers || [], reviewPath: inputs.review?.mdPath };
|
|
206
|
+
case 'refiner':
|
|
207
|
+
return { inPlanPath: inputs.plan?.path, outPlanPath: outputs.plan?.path, reviewJsonPath: outputs.review?.jsonPath, cycle };
|
|
208
|
+
case 'decomposer':
|
|
209
|
+
return { planPath: inputs.plan?.path, decompositionPath: outputs.decomposition?.path };
|
|
210
|
+
case 'implementer':
|
|
211
|
+
// The implementer fixes CODE reviews (impl-review / webui-review). A plan-review
|
|
212
|
+
// left on the shared `review` bus by a planReviewer -> planner loop is NOT for the
|
|
213
|
+
// implementer (it bounces to the planner), so it must not flip a first-pass
|
|
214
|
+
// implementer into fix mode. Discriminate by review provenance (reviewKind), not
|
|
215
|
+
// cycle — a linear `... -> reviewer -> implementer` forward edge legitimately fixes
|
|
216
|
+
// a code review at cycle 1 (see saved-pipeline-parity).
|
|
217
|
+
return { planPath: inputs.plan?.path, reviewPath: inputs.review?.mdPath, mode: (inputs.review?.mdPath && inputs.review?.reviewKind !== 'plan-review') ? 'fix' : 'implement', cycle };
|
|
218
|
+
case 'planReviewer':
|
|
219
|
+
return { planPath: inputs.plan?.path, reviewMdPath: outputs.review?.mdPath, reviewJsonPath: outputs.review?.jsonPath, cycle };
|
|
220
|
+
case 'reviewer':
|
|
221
|
+
return { planPath: inputs.plan?.path, reviewMdPath: outputs.review?.mdPath, reviewJsonPath: outputs.review?.jsonPath, cycle };
|
|
222
|
+
case 'workspaceReviewer':
|
|
223
|
+
// Identical field shape to `reviewer` — the workspace synthesizer reads the
|
|
224
|
+
// same planPath/reviewMd/reviewJson/cycle and folds its merged verdict onto
|
|
225
|
+
// the shared `review` channel its implementer loop target consumes.
|
|
226
|
+
return { planPath: inputs.plan?.path, reviewMdPath: outputs.review?.mdPath, reviewJsonPath: outputs.review?.jsonPath, cycle };
|
|
227
|
+
case 'manualTestsChecklist':
|
|
228
|
+
return { planPath: inputs.plan?.path, checklistPath: outputs.checklist?.path };
|
|
229
|
+
case 'manualWebUiTesting':
|
|
230
|
+
return { checklistPath: inputs.checklist?.path, reviewMdPath: outputs.review?.mdPath, reviewJsonPath: outputs.review?.jsonPath, cycle };
|
|
231
|
+
default:
|
|
232
|
+
// Generic agents: no bespoke field names. The generic runners consume the
|
|
233
|
+
// typed inputs/outputs maps directly (same references _bindNodeIo spreads).
|
|
234
|
+
return { cycle, inputs, outputs };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Channels materializable from plain prompt text (markdown artifacts). userPrompt
|
|
239
|
+
* is already the prompt; code is the standing worktree; review is not pre-seeded and
|
|
240
|
+
* is optional for its only consumer — so none of those ever seed. */
|
|
241
|
+
const SEEDABLE = new Set(['plan', 'checklist']);
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Decide which materializable channels must be seeded from the user prompt because
|
|
245
|
+
* the topology REQUIRES them before any step produces them (a pipeline that starts
|
|
246
|
+
* mid-stream — implementer/refiner/... first). Mirrors the validator's step-ordered
|
|
247
|
+
* reachability walk (workflow-validator.mjs:112-144) but returns the channels to fill
|
|
248
|
+
* rather than warnings. Reads the channel spec carried on each resolved node, so no
|
|
249
|
+
* registry argument is needed (resolveWorkflow already stamped consumes/produces).
|
|
250
|
+
* @param {Array<Array<{key,consumes,optionalConsumes,produces}>>} steps
|
|
251
|
+
* @returns {string[]} channel ids to seed (subset of SEEDABLE), in first-needed order
|
|
252
|
+
*/
|
|
253
|
+
export function entrySeedChannels(steps) {
|
|
254
|
+
const seeded = new Set();
|
|
255
|
+
const produced = new Set();
|
|
256
|
+
for (const group of steps || []) {
|
|
257
|
+
for (const node of group || []) {
|
|
258
|
+
const optional = new Set(node?.optionalConsumes || []);
|
|
259
|
+
for (const c of node?.consumes || []) {
|
|
260
|
+
if (!SEEDABLE.has(c) || optional.has(c) || produced.has(c) || seeded.has(c)) continue;
|
|
261
|
+
seeded.add(c);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Producers fold in AFTER the step (matches the validator's frozen-snapshot model),
|
|
265
|
+
// so a node that both consumes AND produces the same channel (the refiner) still
|
|
266
|
+
// triggers a seed at its own step.
|
|
267
|
+
for (const node of group || []) for (const c of node?.produces || []) produced.add(c);
|
|
268
|
+
}
|
|
269
|
+
return [...seeded];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Render the `## Attached files` block listing each attachment by path + name.
|
|
274
|
+
* Single source of truth shared by renderPromptArtifact (the seeded file body) and
|
|
275
|
+
* phases.mjs taskHeader (the entry agent's inline header) so the two cannot drift.
|
|
276
|
+
* Returns '' when there are no attachments.
|
|
277
|
+
* @param {Array<{name:string,path:string}>} [extras]
|
|
278
|
+
*/
|
|
279
|
+
export function renderAttachmentsBlock(extras = []) {
|
|
280
|
+
if (!Array.isArray(extras) || extras.length === 0) return '';
|
|
281
|
+
return (
|
|
282
|
+
`\n## Attached files\n\nThe user attached these files; read any that are relevant:\n\n` +
|
|
283
|
+
extras.map((e) => `- \`${e.path}\` (${e.name})`).join('\n') +
|
|
284
|
+
'\n'
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Render the markdown a seeded artifact channel holds: the user's request stands in
|
|
290
|
+
* for the missing upstream artifact, with any attached files listed by path.
|
|
291
|
+
* @param {string} promptText
|
|
292
|
+
* @param {Array<{name:string,path:string}>} [extras]
|
|
293
|
+
*/
|
|
294
|
+
export function renderPromptArtifact(promptText, extras = []) {
|
|
295
|
+
const body = (promptText || '').trim() || '(no prompt text)';
|
|
296
|
+
return (
|
|
297
|
+
`# Task (from the user prompt)\n\n` +
|
|
298
|
+
`No upstream agent produced this artifact, so the user's request below stands in for it.\n\n` +
|
|
299
|
+
`## Original request\n\n${body}\n` +
|
|
300
|
+
renderAttachmentsBlock(extras)
|
|
301
|
+
);
|
|
302
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/core/chat/allowlist.mjs
|
|
2
|
+
// Inbound chat-ID gate, ported from pre-1.0 integrations/allowlist.js.
|
|
3
|
+
// DENY BY DEFAULT: an empty allowlist admits nobody — configuring a bot token
|
|
4
|
+
// without allowedChatIds gives outbound notifications but zero inbound control
|
|
5
|
+
// (chat-connectivity-design.md §4.6).
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {string[]} allowedIds chat IDs permitted to send inbound commands
|
|
9
|
+
* @param {{ debug?: (...args: unknown[]) => void }} [log]
|
|
10
|
+
*/
|
|
11
|
+
export function createAllowlistGuard(allowedIds, log = {}) {
|
|
12
|
+
const set = new Set((allowedIds || []).map((s) => String(s).trim()).filter(Boolean));
|
|
13
|
+
const debug = log.debug ?? (() => {});
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
isAllowed({ platform, chatId }) {
|
|
17
|
+
if (set.has(String(chatId))) return true;
|
|
18
|
+
debug(`[allowlist] drop inbound message — platform=${platform} chatId=${chatId} not in allowlist`);
|
|
19
|
+
return false;
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Comma-separated config value -> id list ("123, 456" -> ['123','456']). */
|
|
25
|
+
export function parseIdList(value) {
|
|
26
|
+
return String(value ?? '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
27
|
+
}
|