@yemi33/minions 0.1.2424 → 0.1.2426
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/bin/install-internal-minions.js +1209 -0
- package/bin/minions.js +75 -9
- package/dashboard/docs/typography.md +27 -12
- package/dashboard/js/command-center.js +13 -11
- package/dashboard/js/live-stream.js +1 -1
- package/dashboard/js/memory-search.js +388 -43
- package/dashboard/js/modal-qa.js +5 -5
- package/dashboard/js/qa.js +39 -26
- package/dashboard/js/refresh.js +37 -18
- package/dashboard/js/render-agents.js +14 -18
- package/dashboard/js/render-dispatch.js +14 -74
- package/dashboard/js/render-inbox.js +3 -3
- package/dashboard/js/render-meetings.js +8 -8
- package/dashboard/js/render-other.js +45 -20
- package/dashboard/js/render-pipelines.js +6 -6
- package/dashboard/js/render-plans.js +14 -14
- package/dashboard/js/render-prd.js +46 -46
- package/dashboard/js/render-prs.js +9 -65
- package/dashboard/js/render-schedules.js +5 -5
- package/dashboard/js/render-skills.js +6 -6
- package/dashboard/js/render-utils.js +4 -4
- package/dashboard/js/render-watches.js +4 -16
- package/dashboard/js/render-work-items.js +95 -107
- package/dashboard/js/settings.js +175 -74
- package/dashboard/layout.html +3 -2
- package/dashboard/pages/inbox.html +1 -1
- package/dashboard/pages/work.html +1 -1
- package/dashboard/shared/model-display.js +9 -0
- package/dashboard/shared/watches-source.js +43 -0
- package/dashboard/shared/welcome-popup.js +259 -0
- package/dashboard/slim/body.html +6 -6
- package/dashboard/slim/js/chat.js +4 -2
- package/dashboard/slim/js/history.js +10 -3
- package/dashboard/slim/js/modals-tiles.js +85 -16
- package/dashboard/slim/js/status.js +12 -16
- package/dashboard/slim/styles.css +24 -4
- package/dashboard/styles.css +62 -27
- package/dashboard-build.js +8 -2
- package/dashboard.js +3854 -2514
- package/docs/README.md +7 -5
- package/docs/api-errors.md +144 -0
- package/docs/auto-discovery.md +83 -61
- package/docs/capture-demos.js +173 -30
- package/docs/command-center.md +5 -1
- package/docs/completion-reports.md +127 -11
- package/docs/constants.md +15 -3
- package/docs/constellation-style-telemetry.md +2 -2
- package/docs/copilot-cli-schema.md +32 -22
- package/docs/cross-repo-plans.md +30 -30
- package/docs/demo/memory-system.html +1 -1
- package/docs/deprecated.json +132 -18
- package/docs/diagnostics-memory.md +8 -4
- package/docs/human-vs-automated.md +2 -2
- package/docs/index.html +9 -2
- package/docs/internal-install.md +212 -0
- package/docs/kb-pr3223-cascade-archiving.md +16 -0
- package/docs/kb-pr696-merge-conflict-docs.md +23 -0
- package/docs/kb-sweep.md +2 -2
- package/docs/keep-processes.md +8 -1
- package/docs/live-checkout-mode.md +1 -1
- package/docs/managed-spawn.md +21 -3
- package/docs/named-agents.md +3 -2
- package/docs/onboarding.md +26 -0
- package/docs/plan-lifecycle.md +32 -32
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
- package/docs/qa-runbook-lifecycle.md +67 -51
- package/docs/qa-runbooks.md +2 -2
- package/docs/runtime-adapters.md +101 -50
- package/docs/security.md +28 -0
- package/docs/self-improvement.md +47 -13
- package/docs/skills.md +29 -0
- package/docs/slim-ux/concepts.md +2 -2
- package/docs/specs/agent-configurability.md +43 -41
- package/docs/specs/agent-rename.md +13 -13
- package/docs/team-memory.md +283 -14
- package/docs/tutorials/01-install-and-connect.md +23 -0
- package/docs/tutorials/03-plan-driven-feature.md +3 -3
- package/docs/tutorials/04-runtimes-and-harness.md +18 -21
- package/docs/tutorials/08-operations-and-recovery.md +3 -2
- package/docs/tutorials/README.md +4 -3
- package/docs/visual-evidence-ci.md +103 -0
- package/docs/watches.md +35 -30
- package/docs/worktree-lifecycle.md +151 -13
- package/engine/acp-transport.js +1 -0
- package/engine/ado.js +80 -33
- package/engine/agent-api-validation.js +571 -0
- package/engine/agent-worker-pool.js +7 -6
- package/engine/api-contracts/agent-content.js +761 -0
- package/engine/api-contracts/cc-ops.js +219 -0
- package/engine/api-contracts/config-runtime.js +419 -0
- package/engine/api-contracts/core.js +138 -0
- package/engine/api-contracts/index.js +506 -0
- package/engine/api-contracts/orchestration.js +602 -0
- package/engine/api-contracts/pull-requests.js +323 -0
- package/engine/api-contracts/qa-process.js +764 -0
- package/engine/api-contracts/work-plan-prd.js +494 -0
- package/engine/api-validation.js +543 -0
- package/engine/cc-api-validation.js +586 -0
- package/engine/cc-worker-pool.js +9 -1
- package/engine/cleanup.js +393 -271
- package/engine/cli.js +65 -7
- package/engine/comment-classifier.js +70 -5
- package/engine/consolidation.js +123 -5
- package/engine/content-api-validation.js +606 -0
- package/engine/create-pr-worktree.js +22 -1
- package/engine/db/migrations/027-review-learning-lifecycle.js +162 -0
- package/engine/dispatch.js +33 -0
- package/engine/distribution.js +189 -0
- package/engine/execution-model.js +59 -0
- package/engine/features.js +12 -0
- package/engine/github.js +67 -25
- package/engine/harness-context.js +313 -0
- package/engine/inbox-store.js +1 -1
- package/engine/kb-sweep.js +161 -38
- package/engine/keep-process-sweep.js +43 -3
- package/engine/lifecycle.js +1066 -165
- package/engine/llm.js +4 -0
- package/engine/managed-spawn.js +463 -48
- package/engine/meeting.js +197 -34
- package/engine/memory-retrieval.js +280 -0
- package/engine/memory-store.js +821 -38
- package/engine/model-discovery.js +17 -3
- package/engine/pipeline.js +802 -14
- package/engine/plan-prd-validation.js +745 -0
- package/engine/playbook.js +206 -17
- package/engine/pooled-agent-process.js +6 -1
- package/engine/pr-action.js +1 -1
- package/engine/pr-issue-validation.js +653 -0
- package/engine/pr-resolve.js +75 -13
- package/engine/prd-store.js +39 -6
- package/engine/preflight.js +7 -2
- package/engine/process-utils.js +79 -31
- package/engine/projects.js +198 -12
- package/engine/promotion.js +371 -0
- package/engine/qa-from-prd.js +3 -0
- package/engine/qa-process-validation.js +572 -0
- package/engine/qa-runbooks.js +41 -37
- package/engine/qa-runners/maestro.js +11 -7
- package/engine/qa-runners/playwright.js +37 -14
- package/engine/qa-runners.js +9 -10
- package/engine/qa-runs.js +124 -11
- package/engine/qa-sessions.js +139 -40
- package/engine/quarantine-refs.js +81 -11
- package/engine/queries.js +163 -9
- package/engine/review-learning-backfill.js +482 -0
- package/engine/review-learning.js +1236 -0
- package/engine/runtimes/claude.js +60 -2
- package/engine/runtimes/codex.js +57 -16
- package/engine/runtimes/contract.js +21 -0
- package/engine/runtimes/copilot.js +51 -3
- package/engine/runtimes/index.js +1 -0
- package/engine/scheduler.js +201 -32
- package/engine/settings-validation.js +909 -0
- package/engine/shared.js +705 -208
- package/engine/spawn-agent.js +48 -21
- package/engine/steering-constraints.js +31 -0
- package/engine/steering-store.js +10 -2
- package/engine/steering.js +14 -2
- package/engine/timeout.js +404 -111
- package/engine/untrusted-fence.js +24 -10
- package/engine/watch-actions.js +7 -2
- package/engine/watches.js +353 -51
- package/engine/work-item-validation.js +561 -0
- package/engine/work-items-store.js +61 -1
- package/engine/worktree-gc.js +656 -37
- package/engine/worktree-preflight.js +154 -0
- package/engine.js +2068 -342
- package/package.json +1 -1
- package/playbooks/_pr-description-audit.md +88 -41
- package/playbooks/build-fix-complex.md +3 -3
- package/playbooks/docs.md +2 -2
- package/playbooks/fix.md +18 -18
- package/playbooks/implement.md +18 -18
- package/playbooks/qa-session-draft.md +10 -12
- package/playbooks/qa-session-execute.md +16 -12
- package/playbooks/qa-validate.md +8 -7
- package/playbooks/shared-rules.md +45 -0
- package/playbooks/test.md +4 -0
- package/prompts/cc-system.md +1 -1
- package/skills/check-self-authored-review-comment/SKILL.md +2 -2
|
@@ -64,12 +64,25 @@ function _escapeInnerClosers(content) {
|
|
|
64
64
|
// Strip characters that would break out of the fence header's source="…"
|
|
65
65
|
// attribute. Conservative whitelist — keep ASCII letters/digits and a small
|
|
66
66
|
// set of punctuation that real source attributes need.
|
|
67
|
-
function
|
|
67
|
+
function _sanitizeSourceValue(value) {
|
|
68
68
|
return String(value == null ? '' : value)
|
|
69
69
|
.replace(/[\r\n\t]+/g, ' ')
|
|
70
70
|
.replace(/[<>"'&`]/g, '')
|
|
71
|
-
.replace(/\s+/g, '_')
|
|
72
|
-
|
|
71
|
+
.replace(/\s+/g, '_');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function _sanitizeSourceToken(value) {
|
|
75
|
+
return _sanitizeSourceValue(value).slice(0, 200);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function _sanitizeSourceAttribute(value) {
|
|
79
|
+
const sanitized = _sanitizeSourceValue(value);
|
|
80
|
+
if (sanitized.length <= 200) return sanitized;
|
|
81
|
+
const keyIndex = sanitized.lastIndexOf(':key=');
|
|
82
|
+
if (keyIndex < 0) return sanitized.slice(0, 200);
|
|
83
|
+
const keySuffix = sanitized.slice(keyIndex);
|
|
84
|
+
const prefixLength = Math.max(0, 200 - keySuffix.length);
|
|
85
|
+
return sanitized.slice(0, prefixLength) + keySuffix;
|
|
73
86
|
}
|
|
74
87
|
|
|
75
88
|
/**
|
|
@@ -77,10 +90,10 @@ function _sanitizeSourceToken(value) {
|
|
|
77
90
|
* domain-specific order so source-inspection tests can assert literal output.
|
|
78
91
|
*
|
|
79
92
|
* Known shapes:
|
|
80
|
-
* buildSource('pr-comment', { host, slug, number, author }) →
|
|
81
|
-
* 'pr-comment:<host>:<slug>#<number>:author=<author>' (GitHub)
|
|
82
|
-
* buildSource('pr-comment', { host:'ado', org, project, repo, number, author }) →
|
|
83
|
-
* 'pr-comment:ado:<org>/<project>/<repo>!<number>:author=<author>'
|
|
93
|
+
* buildSource('pr-comment', { host, slug, number, author, key }) →
|
|
94
|
+
* 'pr-comment:<host>:<slug>#<number>:author=<author>:key=<key>' (GitHub)
|
|
95
|
+
* buildSource('pr-comment', { host:'ado', org, project, repo, number, author, key }) →
|
|
96
|
+
* 'pr-comment:ado:<org>/<project>/<repo>!<number>:author=<author>:key=<key>'
|
|
84
97
|
* buildSource('pinned-note', { path }) → 'pinned-note:<path>'
|
|
85
98
|
* buildSource('team-notes', { path }) → 'team-notes:<path>'
|
|
86
99
|
* buildSource('agent-memory', { path }) → 'agent-memory:<path>'
|
|
@@ -100,16 +113,17 @@ function buildSource(kind, parts) {
|
|
|
100
113
|
if (k === 'pr-comment') {
|
|
101
114
|
const host = get('host');
|
|
102
115
|
const author = get('author');
|
|
116
|
+
const key = parts.key == null ? '' : _sanitizeSourceValue(parts.key).slice(0, 240);
|
|
103
117
|
if (host === 'ado') {
|
|
104
118
|
const ref = [get('org'), get('project'), get('repo')].filter(Boolean).join('/');
|
|
105
119
|
const num = get('number');
|
|
106
120
|
const tail = num ? `${ref}!${num}` : ref;
|
|
107
|
-
return [k, host, tail, author && `author=${author}`].filter(Boolean).join(':');
|
|
121
|
+
return [k, host, tail, author && `author=${author}`, key && `key=${key}`].filter(Boolean).join(':');
|
|
108
122
|
}
|
|
109
123
|
const slug = get('slug');
|
|
110
124
|
const num = get('number');
|
|
111
125
|
const tail = num ? `${slug}#${num}` : slug;
|
|
112
|
-
return [k, host, tail, author && `author=${author}`].filter(Boolean).join(':');
|
|
126
|
+
return [k, host, tail, author && `author=${author}`, key && `key=${key}`].filter(Boolean).join(':');
|
|
113
127
|
}
|
|
114
128
|
|
|
115
129
|
if (k === 'pinned-note' || k === 'team-notes' || k === 'agent-memory'
|
|
@@ -176,7 +190,7 @@ function wrapUntrusted(content, source) {
|
|
|
176
190
|
? `${text}\n\n[truncated ${truncatedBytes} more bytes]`
|
|
177
191
|
: text;
|
|
178
192
|
|
|
179
|
-
const srcAttr =
|
|
193
|
+
const srcAttr = _sanitizeSourceAttribute(source || 'untrusted');
|
|
180
194
|
return `${FENCE_OPEN_PREFIX} source="${srcAttr}">${body}${FENCE_CLOSE}`;
|
|
181
195
|
}
|
|
182
196
|
|
package/engine/watch-actions.js
CHANGED
|
@@ -722,12 +722,17 @@ registerActionType(WATCH_ACTION_TYPE.TRIGGER_PIPELINE, {
|
|
|
722
722
|
let pipelineMod;
|
|
723
723
|
try { pipelineMod = require('./pipeline'); }
|
|
724
724
|
catch (err) { return { ok: false, summary: `trigger-pipeline: pipeline module unavailable (${err.message})` }; }
|
|
725
|
-
|
|
725
|
+
let pipeline;
|
|
726
|
+
try { pipeline = pipelineMod.getPipeline(pipelineId); }
|
|
727
|
+
catch (err) { return { ok: false, summary: `trigger-pipeline: invalid pipeline id (${err.message})` }; }
|
|
726
728
|
if (!pipeline) return { ok: false, summary: `trigger-pipeline: pipeline ${pipelineId} not found` };
|
|
727
729
|
if (pipelineMod.getActiveRun(pipelineId)) {
|
|
728
730
|
return { ok: false, summary: `trigger-pipeline: ${pipelineId} already has an active run` };
|
|
729
731
|
}
|
|
730
|
-
const
|
|
732
|
+
const config = shared.safeJson(path.join(shared.MINIONS_DIR, 'config.json')) || {};
|
|
733
|
+
let run;
|
|
734
|
+
try { run = pipelineMod.startValidatedRun(pipelineId, pipeline, config); }
|
|
735
|
+
catch (err) { return { ok: false, summary: `trigger-pipeline: invalid definition (${err.message})` }; }
|
|
731
736
|
if (!run) return { ok: false, summary: `trigger-pipeline: ${pipelineId} startRun returned null` };
|
|
732
737
|
return { ok: true, summary: `triggered pipeline ${pipelineId} run ${run.runId}`, runId: run.runId };
|
|
733
738
|
},
|
package/engine/watches.js
CHANGED
|
@@ -70,11 +70,23 @@ const shared = require('./shared');
|
|
|
70
70
|
const { mutateWatches, ts, uid, log, writeToInbox,
|
|
71
71
|
WATCH_STATUS, WATCH_TARGET_TYPE, WATCH_CONDITION, WATCH_ABSOLUTE_CONDITIONS } = shared;
|
|
72
72
|
const watchActions = require('./watch-actions');
|
|
73
|
+
const apiValidation = require('./api-validation');
|
|
73
74
|
|
|
74
75
|
// Default check interval: 5 minutes (300000ms). Engine tick runs every 10s
|
|
75
76
|
// (W-mpxpckey000laa4f), so watches are checked every N ticks where
|
|
76
77
|
// N = ceil(interval / tickInterval).
|
|
77
78
|
const DEFAULT_WATCH_INTERVAL = 300000;
|
|
79
|
+
const WATCH_LIMITS = Object.freeze({
|
|
80
|
+
minIntervalMs: 60000,
|
|
81
|
+
maxIntervalMs: Number.MAX_SAFE_INTEGER,
|
|
82
|
+
maxStopAfter: Number.MAX_SAFE_INTEGER,
|
|
83
|
+
maxTargetStringLength: 4096,
|
|
84
|
+
maxTargetBytes: 16 * 1024,
|
|
85
|
+
maxDescriptionLength: 4000,
|
|
86
|
+
maxRequirements: 20,
|
|
87
|
+
});
|
|
88
|
+
const WATCH_NOTIFY_MODES = Object.freeze(['inbox', 'none']);
|
|
89
|
+
const WATCH_ON_NOT_MET_MODES = Object.freeze(['notify']);
|
|
78
90
|
|
|
79
91
|
// P-w12d8f3a — Phase 6.2: per-watch evaluation history is capped at 25
|
|
80
92
|
// entries (oldest-first). Bounded to keep watch state compact:
|
|
@@ -104,6 +116,7 @@ const TARGET_TYPES = {};
|
|
|
104
116
|
* @param {object} spec - { label, conditions, absoluteConditions?, fetchEntity, captureState, evaluate }
|
|
105
117
|
* - label: string shown in dashboard pickers
|
|
106
118
|
* - conditions: array of condition strings the type accepts
|
|
119
|
+
* - validateTarget: (target, { condition, targetType }) => null-or-error-message
|
|
107
120
|
* - absoluteConditions: (optional) array of condition keys that should fire
|
|
108
121
|
* once and auto-expire when stopAfter=0. Normalized
|
|
109
122
|
* into a Set on the spec; defaults to an empty Set.
|
|
@@ -128,6 +141,9 @@ function registerTargetType(type, spec) {
|
|
|
128
141
|
if (!Array.isArray(spec.conditions) || spec.conditions.length === 0) {
|
|
129
142
|
throw new Error(`registerTargetType(${type}): spec.conditions must be a non-empty array`);
|
|
130
143
|
}
|
|
144
|
+
if (spec.validateTarget !== undefined && typeof spec.validateTarget !== 'function') {
|
|
145
|
+
throw new Error(`registerTargetType(${type}): spec.validateTarget must be a function`);
|
|
146
|
+
}
|
|
131
147
|
// W-mp7hg58e000b5212 — normalize absoluteConditions into a Set for fast
|
|
132
148
|
// .has() lookup at evaluate-time. Accepts an array (declarative spec form)
|
|
133
149
|
// or pre-built Set; defaults to empty when omitted. Entries must be a
|
|
@@ -173,6 +189,295 @@ function listTargetTypes() {
|
|
|
173
189
|
}));
|
|
174
190
|
}
|
|
175
191
|
|
|
192
|
+
function _watchInputError(message, code, path, rejectedValue) {
|
|
193
|
+
throw new apiValidation.ApiInputError(message, {
|
|
194
|
+
code,
|
|
195
|
+
field: Array.isArray(path) ? path[path.length - 1] : path,
|
|
196
|
+
path,
|
|
197
|
+
rejectedValue,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function _validateWatchEnum(value, values, options) {
|
|
202
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
203
|
+
_watchInputError(
|
|
204
|
+
`${options.path || options.field} has no configured allowed values`,
|
|
205
|
+
'invalid-enum',
|
|
206
|
+
options.path || options.field,
|
|
207
|
+
value,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
return apiValidation.validateEnum(value, { ...options, values });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function _validateWatchTarget(target, targetType, path = 'target', condition = null) {
|
|
214
|
+
const spec = TARGET_TYPES[targetType];
|
|
215
|
+
let normalized = target;
|
|
216
|
+
if (typeof target === 'string') {
|
|
217
|
+
normalized = apiValidation.validateString(target, {
|
|
218
|
+
field: 'target',
|
|
219
|
+
path,
|
|
220
|
+
trim: true,
|
|
221
|
+
minLength: 1,
|
|
222
|
+
maxLength: WATCH_LIMITS.maxTargetStringLength,
|
|
223
|
+
});
|
|
224
|
+
} else if (target && typeof target === 'object' && !Array.isArray(target)) {
|
|
225
|
+
apiValidation.validatePlainObject(target, { field: 'target', path });
|
|
226
|
+
if (Object.keys(target).length === 0) {
|
|
227
|
+
_watchInputError(`${path} must not be empty`, 'invalid-target', path, target);
|
|
228
|
+
}
|
|
229
|
+
let serialized;
|
|
230
|
+
try {
|
|
231
|
+
serialized = JSON.stringify(target);
|
|
232
|
+
} catch {
|
|
233
|
+
_watchInputError(`${path} must be JSON-serializable`, 'invalid-target', path, target);
|
|
234
|
+
}
|
|
235
|
+
if (Buffer.byteLength(serialized, 'utf8') > WATCH_LIMITS.maxTargetBytes) {
|
|
236
|
+
_watchInputError(
|
|
237
|
+
`${path} exceeds the ${WATCH_LIMITS.maxTargetBytes}-byte limit`,
|
|
238
|
+
'invalid-target',
|
|
239
|
+
path,
|
|
240
|
+
target,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
_watchInputError(`${path} must be a non-empty string or object`, 'invalid-target', path, target);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (spec && typeof spec.validateTarget === 'function') {
|
|
248
|
+
let targetError;
|
|
249
|
+
try {
|
|
250
|
+
targetError = spec.validateTarget(normalized, { condition, targetType });
|
|
251
|
+
} catch (err) {
|
|
252
|
+
if (apiValidation.isApiInputError(err)) throw err;
|
|
253
|
+
targetError = err && err.message ? err.message : 'target rejected by target-type validator';
|
|
254
|
+
}
|
|
255
|
+
if (targetError === false) targetError = 'target rejected by target-type validator';
|
|
256
|
+
if (typeof targetError === 'string' && targetError.trim()) {
|
|
257
|
+
_watchInputError(targetError.trim(), 'invalid-target', path, target);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return normalized;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function _validateWatchInterval(value, path = 'interval') {
|
|
264
|
+
if (value === undefined || value === null) return DEFAULT_WATCH_INTERVAL;
|
|
265
|
+
apiValidation.validateInteger(value, { field: 'interval', path });
|
|
266
|
+
return apiValidation.validateRange(value, {
|
|
267
|
+
field: 'interval',
|
|
268
|
+
path,
|
|
269
|
+
min: WATCH_LIMITS.minIntervalMs,
|
|
270
|
+
max: WATCH_LIMITS.maxIntervalMs,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function _validateWatchStopAfter(value, path = 'stopAfter') {
|
|
275
|
+
if (value === undefined || value === null) return 0;
|
|
276
|
+
apiValidation.validateInteger(value, { field: 'stopAfter', path });
|
|
277
|
+
return apiValidation.validateRange(value, {
|
|
278
|
+
field: 'stopAfter',
|
|
279
|
+
path,
|
|
280
|
+
min: 0,
|
|
281
|
+
max: WATCH_LIMITS.maxStopAfter,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function _validateWatchAction(action, path = 'action') {
|
|
286
|
+
if (action === undefined || action === null) return action ?? null;
|
|
287
|
+
const priorityError = watchActions.validateActionWorkItemPriority(action);
|
|
288
|
+
if (priorityError) {
|
|
289
|
+
throw new apiValidation.ApiInputError(priorityError.error, {
|
|
290
|
+
code: priorityError.code,
|
|
291
|
+
field: 'priority',
|
|
292
|
+
path,
|
|
293
|
+
rejectedValue: priorityError.rejectedValue,
|
|
294
|
+
allowedValues: priorityError.allowedValues,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
const actionError = watchActions.validateAction(action);
|
|
298
|
+
if (actionError) _watchInputError(actionError, 'invalid-action', path, action);
|
|
299
|
+
return action;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function _validateWatchRequires(requires, path = 'requires') {
|
|
303
|
+
if (requires === undefined || requires === null) return requires ?? null;
|
|
304
|
+
const requirementError = _validateRequires(requires);
|
|
305
|
+
if (requirementError) {
|
|
306
|
+
_watchInputError(requirementError, 'invalid-requirement', path, requires);
|
|
307
|
+
}
|
|
308
|
+
return requires.map(requirement => ({
|
|
309
|
+
target: requirement.target,
|
|
310
|
+
targetType: requirement.targetType,
|
|
311
|
+
condition: requirement.condition,
|
|
312
|
+
}));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function validateWatchCreateInput(input, config) {
|
|
316
|
+
apiValidation.validatePlainObject(input, { field: 'body', path: 'body' });
|
|
317
|
+
const targetType = _validateWatchEnum(input.targetType, Object.keys(TARGET_TYPES).sort(), {
|
|
318
|
+
field: 'targetType',
|
|
319
|
+
path: 'targetType',
|
|
320
|
+
});
|
|
321
|
+
const condition = _validateWatchEnum(input.condition, [...TARGET_TYPES[targetType].conditions], {
|
|
322
|
+
field: 'condition',
|
|
323
|
+
path: 'condition',
|
|
324
|
+
message: `condition must be one of: ${TARGET_TYPES[targetType].conditions.join(', ')} (for targetType ${targetType})`,
|
|
325
|
+
});
|
|
326
|
+
const target = _validateWatchTarget(input.target, targetType, 'target', condition);
|
|
327
|
+
const interval = _validateWatchInterval(input.interval);
|
|
328
|
+
const stopAfter = _validateWatchStopAfter(input.stopAfter);
|
|
329
|
+
|
|
330
|
+
let owner = input.owner;
|
|
331
|
+
if (owner === undefined || owner === null || owner === '') owner = 'human';
|
|
332
|
+
owner = apiValidation.validateIdentifier(owner, { field: 'owner', path: 'owner', trim: true });
|
|
333
|
+
if (config && typeof config === 'object') {
|
|
334
|
+
_validateWatchEnum(owner, ['human', ...Object.keys(config.agents || {})], {
|
|
335
|
+
field: 'owner',
|
|
336
|
+
path: 'owner',
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
let project = input.project;
|
|
341
|
+
if (project === undefined || project === null || project === '') {
|
|
342
|
+
project = null;
|
|
343
|
+
} else {
|
|
344
|
+
project = apiValidation.validateString(project, {
|
|
345
|
+
field: 'project',
|
|
346
|
+
path: 'project',
|
|
347
|
+
trim: true,
|
|
348
|
+
minLength: 1,
|
|
349
|
+
maxLength: 128,
|
|
350
|
+
});
|
|
351
|
+
if (config && typeof config === 'object') {
|
|
352
|
+
_validateWatchEnum(
|
|
353
|
+
project,
|
|
354
|
+
(config.projects || []).map(candidate => candidate && candidate.name).filter(Boolean),
|
|
355
|
+
{
|
|
356
|
+
field: 'project',
|
|
357
|
+
path: 'project',
|
|
358
|
+
},
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
let description = input.description;
|
|
364
|
+
if (description === undefined || description === null || description === '') {
|
|
365
|
+
description = null;
|
|
366
|
+
} else {
|
|
367
|
+
description = apiValidation.validateString(description, {
|
|
368
|
+
field: 'description',
|
|
369
|
+
path: 'description',
|
|
370
|
+
trim: true,
|
|
371
|
+
maxLength: WATCH_LIMITS.maxDescriptionLength,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
let notify = input.notify;
|
|
376
|
+
if (notify === undefined || notify === null || notify === '') notify = 'inbox';
|
|
377
|
+
notify = _validateWatchEnum(notify, [...WATCH_NOTIFY_MODES], {
|
|
378
|
+
field: 'notify',
|
|
379
|
+
path: 'notify',
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
let onNotMet = input.onNotMet;
|
|
383
|
+
if (onNotMet === undefined || onNotMet === null || onNotMet === '') {
|
|
384
|
+
onNotMet = null;
|
|
385
|
+
} else {
|
|
386
|
+
onNotMet = _validateWatchEnum(onNotMet, [...WATCH_ON_NOT_MET_MODES], {
|
|
387
|
+
field: 'onNotMet',
|
|
388
|
+
path: 'onNotMet',
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
target,
|
|
394
|
+
targetType,
|
|
395
|
+
condition,
|
|
396
|
+
interval,
|
|
397
|
+
owner,
|
|
398
|
+
description,
|
|
399
|
+
project,
|
|
400
|
+
notify,
|
|
401
|
+
stopAfter,
|
|
402
|
+
onNotMet,
|
|
403
|
+
action: _validateWatchAction(input.action),
|
|
404
|
+
requires: _validateWatchRequires(input.requires),
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function validateWatchUpdateInput(updates, existingWatch = null) {
|
|
409
|
+
apiValidation.validatePlainObject(updates, { field: 'updates', path: 'updates' });
|
|
410
|
+
const normalized = { ...updates };
|
|
411
|
+
if (updates.status !== undefined) {
|
|
412
|
+
normalized.status = _validateWatchEnum(updates.status, Object.values(WATCH_STATUS), {
|
|
413
|
+
field: 'status',
|
|
414
|
+
path: 'status',
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
if (updates.interval !== undefined) normalized.interval = _validateWatchInterval(updates.interval);
|
|
418
|
+
if (updates.stopAfter !== undefined) normalized.stopAfter = _validateWatchStopAfter(updates.stopAfter);
|
|
419
|
+
if (updates.description !== undefined) {
|
|
420
|
+
normalized.description = updates.description === null
|
|
421
|
+
? null
|
|
422
|
+
: apiValidation.validateString(updates.description, {
|
|
423
|
+
field: 'description',
|
|
424
|
+
path: 'description',
|
|
425
|
+
trim: true,
|
|
426
|
+
maxLength: WATCH_LIMITS.maxDescriptionLength,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
if (updates.notify !== undefined) {
|
|
430
|
+
normalized.notify = _validateWatchEnum(updates.notify, [...WATCH_NOTIFY_MODES], {
|
|
431
|
+
field: 'notify',
|
|
432
|
+
path: 'notify',
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
if (updates.onNotMet !== undefined) {
|
|
436
|
+
normalized.onNotMet = updates.onNotMet === null || updates.onNotMet === ''
|
|
437
|
+
? null
|
|
438
|
+
: _validateWatchEnum(updates.onNotMet, [...WATCH_ON_NOT_MET_MODES], {
|
|
439
|
+
field: 'onNotMet',
|
|
440
|
+
path: 'onNotMet',
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
if (updates.condition !== undefined) {
|
|
444
|
+
const condition = apiValidation.validateString(updates.condition, {
|
|
445
|
+
field: 'condition',
|
|
446
|
+
path: 'condition',
|
|
447
|
+
trim: true,
|
|
448
|
+
minLength: 1,
|
|
449
|
+
maxLength: 128,
|
|
450
|
+
});
|
|
451
|
+
normalized.condition = existingWatch
|
|
452
|
+
? _validateWatchEnum(condition, [...(TARGET_TYPES[existingWatch.targetType]?.conditions || [])], {
|
|
453
|
+
field: 'condition',
|
|
454
|
+
path: 'condition',
|
|
455
|
+
})
|
|
456
|
+
: condition;
|
|
457
|
+
if (existingWatch) {
|
|
458
|
+
_validateWatchTarget(
|
|
459
|
+
existingWatch.target,
|
|
460
|
+
existingWatch.targetType,
|
|
461
|
+
'target',
|
|
462
|
+
normalized.condition,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (updates.action !== undefined) normalized.action = _validateWatchAction(updates.action);
|
|
467
|
+
if (updates.requires !== undefined) normalized.requires = _validateWatchRequires(updates.requires);
|
|
468
|
+
return normalized;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function validateWatchId(value) {
|
|
472
|
+
return apiValidation.validateIdentifier(value, {
|
|
473
|
+
field: 'id',
|
|
474
|
+
path: 'id',
|
|
475
|
+
trim: true,
|
|
476
|
+
maxLength: 128,
|
|
477
|
+
pattern: /^watch-[A-Za-z0-9_-]+$/,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
176
481
|
/**
|
|
177
482
|
* Read all watches from SQLite.
|
|
178
483
|
* @returns {Array<object>}
|
|
@@ -186,44 +491,26 @@ function getWatches() {
|
|
|
186
491
|
* @param {object} opts - Watch definition
|
|
187
492
|
* @returns {object} - Created watch
|
|
188
493
|
*/
|
|
189
|
-
function createWatch(
|
|
190
|
-
|
|
191
|
-
if (!targetType || !TARGET_TYPES[targetType]) {
|
|
192
|
-
throw new Error(`targetType must be one of: ${Object.keys(TARGET_TYPES).sort().join(', ')}`);
|
|
193
|
-
}
|
|
194
|
-
const tt = TARGET_TYPES[targetType];
|
|
195
|
-
if (!condition || !tt.conditions.includes(condition)) {
|
|
196
|
-
throw new Error(`condition must be one of: ${tt.conditions.join(', ')} (for targetType ${targetType})`);
|
|
197
|
-
}
|
|
198
|
-
if (action !== undefined && action !== null) {
|
|
199
|
-
const actionErr = watchActions.validateAction(action);
|
|
200
|
-
if (actionErr) throw new Error(actionErr);
|
|
201
|
-
}
|
|
202
|
-
// P-w10b6e2d — Phase 5: validate optional requires[] cross-target join.
|
|
203
|
-
if (requires !== undefined && requires !== null) {
|
|
204
|
-
const reqErr = _validateRequires(requires);
|
|
205
|
-
if (reqErr) throw new Error(reqErr);
|
|
206
|
-
}
|
|
494
|
+
function createWatch(input, config) {
|
|
495
|
+
const validated = validateWatchCreateInput(input, config);
|
|
207
496
|
|
|
208
497
|
const watch = {
|
|
209
498
|
id: 'watch-' + uid(),
|
|
210
|
-
target,
|
|
211
|
-
targetType,
|
|
212
|
-
condition,
|
|
213
|
-
interval:
|
|
214
|
-
owner: owner
|
|
499
|
+
target: validated.target,
|
|
500
|
+
targetType: validated.targetType,
|
|
501
|
+
condition: validated.condition,
|
|
502
|
+
interval: validated.interval,
|
|
503
|
+
owner: validated.owner,
|
|
215
504
|
status: WATCH_STATUS.ACTIVE,
|
|
216
|
-
description: description || `Watch ${target} for ${condition}`,
|
|
217
|
-
project: project
|
|
218
|
-
notify: notify
|
|
219
|
-
stopAfter:
|
|
220
|
-
onNotMet: onNotMet
|
|
221
|
-
action: action
|
|
505
|
+
description: validated.description || `Watch ${validated.target} for ${validated.condition}`,
|
|
506
|
+
project: validated.project,
|
|
507
|
+
notify: validated.notify,
|
|
508
|
+
stopAfter: validated.stopAfter, // 0 = run forever; N = expire after N triggers
|
|
509
|
+
onNotMet: validated.onNotMet, // null | 'notify' — action per poll when condition not met
|
|
510
|
+
action: validated.action, // optional follow-up action — see engine/watch-actions.js
|
|
222
511
|
// P-w10b6e2d — Phase 5: optional requires[] — when present every entry
|
|
223
512
|
// must also evaluate truthy for the watch to fire. See evaluateWatch.
|
|
224
|
-
requires:
|
|
225
|
-
target: r.target, targetType: r.targetType, condition: r.condition,
|
|
226
|
-
})) : null,
|
|
513
|
+
requires: validated.requires,
|
|
227
514
|
triggerCount: 0,
|
|
228
515
|
created_at: ts(),
|
|
229
516
|
last_checked: null,
|
|
@@ -252,21 +539,9 @@ function createWatch({ target, targetType, condition, interval, owner, descripti
|
|
|
252
539
|
*/
|
|
253
540
|
function updateWatch(id, updates) {
|
|
254
541
|
if (!id) throw new Error('id is required');
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
260
|
-
if (updates.action !== undefined && updates.action !== null) {
|
|
261
|
-
const actionErr = watchActions.validateAction(updates.action);
|
|
262
|
-
if (actionErr) throw new Error(actionErr);
|
|
263
|
-
}
|
|
264
|
-
// P-w10b6e2d — Phase 5: validate requires[] before entering the lock so a
|
|
265
|
-
// bad shape never lands on disk. `[]` is allowed (clears the join).
|
|
266
|
-
if (updates.requires !== undefined && updates.requires !== null) {
|
|
267
|
-
const reqErr = _validateRequires(updates.requires);
|
|
268
|
-
if (reqErr) throw new Error(reqErr);
|
|
269
|
-
}
|
|
542
|
+
const existing = getWatches().find(watch => watch.id === id);
|
|
543
|
+
const normalized = validateWatchUpdateInput(updates, existing);
|
|
544
|
+
if (!existing) return null;
|
|
270
545
|
let found = null;
|
|
271
546
|
mutateWatches((watches) => {
|
|
272
547
|
const watch = watches.find(w => w.id === id);
|
|
@@ -274,7 +549,7 @@ function updateWatch(id, updates) {
|
|
|
274
549
|
// Only allow safe field updates
|
|
275
550
|
const allowed = ['status', 'interval', 'description', 'notify', 'stopAfter', 'onNotMet', 'condition', 'action', 'requires'];
|
|
276
551
|
for (const key of allowed) {
|
|
277
|
-
if (
|
|
552
|
+
if (normalized[key] !== undefined) watch[key] = normalized[key];
|
|
278
553
|
}
|
|
279
554
|
found = { ...watch };
|
|
280
555
|
return watches;
|
|
@@ -376,23 +651,32 @@ function evaluateWatch(watch, state) {
|
|
|
376
651
|
*/
|
|
377
652
|
function _validateRequires(requires) {
|
|
378
653
|
if (!Array.isArray(requires)) return 'requires must be an array of {target, targetType, condition}';
|
|
654
|
+
if (requires.length > WATCH_LIMITS.maxRequirements) {
|
|
655
|
+
return `requires must contain at most ${WATCH_LIMITS.maxRequirements} entries`;
|
|
656
|
+
}
|
|
657
|
+
const seen = new Set();
|
|
379
658
|
for (let i = 0; i < requires.length; i++) {
|
|
380
659
|
const r = requires[i];
|
|
381
660
|
if (!r || typeof r !== 'object' || Array.isArray(r)) {
|
|
382
661
|
return `requires[${i}] must be a {target, targetType, condition} object`;
|
|
383
662
|
}
|
|
384
|
-
if (!r.target || typeof r.target !== 'string') {
|
|
385
|
-
return `requires[${i}].target is required (non-empty string)`;
|
|
386
|
-
}
|
|
387
663
|
if (!r.targetType || !TARGET_TYPES[r.targetType]) {
|
|
388
664
|
return `requires[${i}].targetType must be one of: ${Object.keys(TARGET_TYPES).sort().join(', ')}`;
|
|
389
665
|
}
|
|
390
666
|
if (!r.condition || !TARGET_TYPES[r.targetType].conditions.includes(r.condition)) {
|
|
391
667
|
return `requires[${i}].condition must be one of: ${TARGET_TYPES[r.targetType].conditions.join(', ')} (for targetType ${r.targetType})`;
|
|
392
668
|
}
|
|
669
|
+
try {
|
|
670
|
+
_validateWatchTarget(r.target, r.targetType, `requires[${i}].target`, r.condition);
|
|
671
|
+
} catch (err) {
|
|
672
|
+
return err && err.message ? err.message : `requires[${i}].target is invalid`;
|
|
673
|
+
}
|
|
393
674
|
if (r.requires !== undefined) {
|
|
394
675
|
return `requires[${i}] must not contain a nested 'requires' field (depth cap is 1)`;
|
|
395
676
|
}
|
|
677
|
+
const identity = JSON.stringify([r.targetType, r.target, r.condition]);
|
|
678
|
+
if (seen.has(identity)) return `requires[${i}] duplicates an earlier requirement`;
|
|
679
|
+
seen.add(identity);
|
|
396
680
|
}
|
|
397
681
|
return null;
|
|
398
682
|
}
|
|
@@ -720,6 +1004,10 @@ function _captureState(watch, state) {
|
|
|
720
1004
|
|
|
721
1005
|
// ── Built-in target type registrations ───────────────────────────────────────
|
|
722
1006
|
|
|
1007
|
+
function _validateStringTarget(target) {
|
|
1008
|
+
return typeof target === 'string' ? null : 'target must be a non-empty string identifier';
|
|
1009
|
+
}
|
|
1010
|
+
|
|
723
1011
|
// PR — pull request status / build / comments / votes
|
|
724
1012
|
registerTargetType(WATCH_TARGET_TYPE.PR, {
|
|
725
1013
|
label: 'Pull Request',
|
|
@@ -742,6 +1030,7 @@ registerTargetType(WATCH_TARGET_TYPE.PR, {
|
|
|
742
1030
|
WATCH_CONDITION.MERGED, WATCH_CONDITION.BUILD_FAIL, WATCH_CONDITION.BUILD_PASS,
|
|
743
1031
|
WATCH_CONDITION.READY_FOR_MERGE,
|
|
744
1032
|
],
|
|
1033
|
+
validateTarget: _validateStringTarget,
|
|
745
1034
|
// W-mqa63opd000ha836 — zombie-watch janitor: once a PR reaches a terminal
|
|
746
1035
|
// status (merged / closed / abandoned), most PR conditions can never fire
|
|
747
1036
|
// again so the watch should auto-expire instead of polling forever.
|
|
@@ -894,6 +1183,7 @@ registerTargetType(WATCH_TARGET_TYPE.WORK_ITEM, {
|
|
|
894
1183
|
WATCH_CONDITION.COMPLETED, WATCH_CONDITION.FAILED,
|
|
895
1184
|
WATCH_CONDITION.RETRY_LIMIT_REACHED,
|
|
896
1185
|
],
|
|
1186
|
+
validateTarget: _validateStringTarget,
|
|
897
1187
|
fetchEntity: (target, state) => (state.workItems || []).find(w => w.id === target) || null,
|
|
898
1188
|
// P-w2c8d1e7 — Phase 1.2: surface retry count, pending-reason, and branch
|
|
899
1189
|
// for upcoming stalled / branch-aware predicates. `retries` aliases the
|
|
@@ -984,6 +1274,7 @@ registerTargetType(WATCH_TARGET_TYPE.MEETING, {
|
|
|
984
1274
|
description: 'Watch a meeting for conclusion or status changes',
|
|
985
1275
|
conditions: [WATCH_CONDITION.CONCLUDED, WATCH_CONDITION.STATUS_CHANGE, WATCH_CONDITION.ANY],
|
|
986
1276
|
absoluteConditions: [WATCH_CONDITION.CONCLUDED],
|
|
1277
|
+
validateTarget: _validateStringTarget,
|
|
987
1278
|
fetchEntity: (target, state) => (state.meetings || []).find(m => m && m.id === target) || null,
|
|
988
1279
|
captureState: (m) => ({ status: m.status }),
|
|
989
1280
|
evaluate: (condition, m, prevState, target) => {
|
|
@@ -1058,6 +1349,7 @@ registerTargetType(WATCH_TARGET_TYPE.PLAN, {
|
|
|
1058
1349
|
WATCH_CONDITION.APPROVED, WATCH_CONDITION.REJECTED, WATCH_CONDITION.COMPLETED,
|
|
1059
1350
|
WATCH_CONDITION.ALL_ITEMS_DONE, WATCH_CONDITION.ITEM_FAILED_N_TIMES,
|
|
1060
1351
|
],
|
|
1352
|
+
validateTarget: _validateStringTarget,
|
|
1061
1353
|
fetchEntity: _findPlan,
|
|
1062
1354
|
// P-w2c8d1e7 — Phase 1.2: surface item progress derived from the PRD's
|
|
1063
1355
|
// missing_features array so plan-level predicates (e.g. all-items-done)
|
|
@@ -1136,6 +1428,7 @@ registerTargetType(WATCH_TARGET_TYPE.SCHEDULE, {
|
|
|
1136
1428
|
// W-mp7hg58e000b5212 — schedule has no absolute conditions today; all of
|
|
1137
1429
|
// its keywords are change-based (RAN/ENABLED/DISABLED detect transitions).
|
|
1138
1430
|
absoluteConditions: [],
|
|
1431
|
+
validateTarget: _validateStringTarget,
|
|
1139
1432
|
fetchEntity: _findSchedule,
|
|
1140
1433
|
// P-w2c8d1e7 — Phase 1.2: surface next_run as null. Cron parsing in
|
|
1141
1434
|
// engine/scheduler.js exposes parseCronExpr/.matches() but not a
|
|
@@ -1221,6 +1514,7 @@ registerTargetType(WATCH_TARGET_TYPE.PIPELINE, {
|
|
|
1221
1514
|
],
|
|
1222
1515
|
// W-mp7hg58e000b5212 — per-target absolute (fire-once when stopAfter=0).
|
|
1223
1516
|
absoluteConditions: [WATCH_CONDITION.COMPLETED, WATCH_CONDITION.FAILED],
|
|
1517
|
+
validateTarget: _validateStringTarget,
|
|
1224
1518
|
fetchEntity: _findPipelineLatestRun,
|
|
1225
1519
|
// P-w2c8d1e7 — Phase 1.2: surface current_stage_id (first non-terminal
|
|
1226
1520
|
// stage in declaration order) for stage-targeted predicates.
|
|
@@ -1315,6 +1609,7 @@ registerTargetType(WATCH_TARGET_TYPE.DISPATCH, {
|
|
|
1315
1609
|
],
|
|
1316
1610
|
// W-mp7hg58e000b5212 — per-target absolute (fire-once when stopAfter=0).
|
|
1317
1611
|
absoluteConditions: [WATCH_CONDITION.COMPLETED, WATCH_CONDITION.FAILED],
|
|
1612
|
+
validateTarget: _validateStringTarget,
|
|
1318
1613
|
fetchEntity: _findDispatchEntry,
|
|
1319
1614
|
captureState: (e) => ({ status: e.status || e._list || null, list: e._list || null }),
|
|
1320
1615
|
evaluate: (condition, e, prevState, target) => {
|
|
@@ -1359,6 +1654,7 @@ registerTargetType(WATCH_TARGET_TYPE.AGENT, {
|
|
|
1359
1654
|
// W-mp7hg58e000b5212 — agent has no absolute conditions today; all of its
|
|
1360
1655
|
// keywords are change-based.
|
|
1361
1656
|
absoluteConditions: [],
|
|
1657
|
+
validateTarget: _validateStringTarget,
|
|
1362
1658
|
fetchEntity: _findAgent,
|
|
1363
1659
|
// P-w2c8d1e7 — Phase 1.2: surface currentDispatchId so predicates can
|
|
1364
1660
|
// distinguish "agent now working on a different dispatch" from a plain
|
|
@@ -1437,10 +1733,16 @@ _loadPluginTargetTypes();
|
|
|
1437
1733
|
|
|
1438
1734
|
module.exports = {
|
|
1439
1735
|
DEFAULT_WATCH_INTERVAL,
|
|
1736
|
+
WATCH_LIMITS,
|
|
1737
|
+
WATCH_NOTIFY_MODES,
|
|
1738
|
+
WATCH_ON_NOT_MET_MODES,
|
|
1440
1739
|
getWatches,
|
|
1441
1740
|
createWatch,
|
|
1442
1741
|
updateWatch,
|
|
1443
1742
|
deleteWatch,
|
|
1743
|
+
validateWatchCreateInput,
|
|
1744
|
+
validateWatchUpdateInput,
|
|
1745
|
+
validateWatchId,
|
|
1444
1746
|
evaluateWatch,
|
|
1445
1747
|
checkWatches,
|
|
1446
1748
|
registerTargetType,
|