atris 3.31.0 → 3.33.3
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 +46 -4
- package/atris/CLAUDE.md +8 -0
- package/atris.md +8 -0
- package/ax +458 -15
- package/bin/atris.js +200 -76
- package/commands/autoland.js +78 -12
- package/commands/autopilot-front.js +273 -0
- package/commands/engine.js +299 -0
- package/commands/init.js +21 -0
- package/commands/integrations.js +147 -0
- package/commands/member.js +85 -0
- package/commands/mission.js +500 -43
- package/commands/run-front.js +144 -0
- package/commands/run.js +9 -6
- package/commands/sign.js +90 -0
- package/commands/task.js +380 -20
- package/commands/truth.js +72 -11
- package/lib/autoland.js +133 -25
- package/lib/fleet.js +354 -0
- package/lib/inspect-fields.js +174 -0
- package/lib/operator-next.js +7 -0
- package/lib/pulse.js +4 -3
- package/lib/runner-command.js +54 -11
- package/lib/task-db.js +57 -2
- package/package.json +3 -2
- package/scripts/agent_worktree.py +72 -0
package/commands/truth.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
// atris truth — one table of what is actually proven, blocked, or stale.
|
|
2
2
|
// Rolls up sources that already exist; writes nothing. SQL/state is truth, this is the render.
|
|
3
3
|
// 1. .atris/state/missions.jsonl — mission states (dedupe by id, latest wins)
|
|
4
|
-
// 2. ~/.atris/tasks.db — task counts by status
|
|
4
|
+
// 2. ~/.atris/tasks.db — task counts by status, scoped to this workspace unless --all is passed
|
|
5
5
|
// 3. atris/features/*/ — validate.md frontmatter + newest proof/ receipt age
|
|
6
6
|
// 4. ~/.atris/heartbeat/{registry,state}.json — declared loops vs last_run / fails
|
|
7
7
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const os = require('os');
|
|
11
|
+
const { execFileSync } = require('child_process');
|
|
12
|
+
const taskDb = require('../lib/task-db');
|
|
11
13
|
|
|
12
14
|
const STALE_DAYS = 7;
|
|
13
15
|
|
|
@@ -38,19 +40,71 @@ function loadMissions(cwd) {
|
|
|
38
40
|
if (id) seen.set(id, rec);
|
|
39
41
|
} catch { /* skip bad lines */ }
|
|
40
42
|
}
|
|
41
|
-
|
|
43
|
+
// stopped = killed-on-purpose = a closed loop; only genuinely open states count
|
|
44
|
+
return [...seen.values()].filter((m) => m.status && !['complete', 'archived', 'stopped'].includes(m.status));
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
function
|
|
47
|
+
function gitCommonWorkspaceRoot(cwd) {
|
|
48
|
+
try {
|
|
49
|
+
const common = execFileSync('git', ['rev-parse', '--git-common-dir'], {
|
|
50
|
+
cwd: cwd || process.cwd(),
|
|
51
|
+
encoding: 'utf8',
|
|
52
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
53
|
+
}).trim();
|
|
54
|
+
if (!common) return null;
|
|
55
|
+
const gitDir = path.resolve(cwd || process.cwd(), common);
|
|
56
|
+
return path.basename(gitDir) === '.git' ? path.dirname(gitDir) : null;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function workspaceRootCandidates(cwd) {
|
|
63
|
+
const primary = taskDb.workspaceRoot(cwd || process.cwd());
|
|
64
|
+
const candidates = [primary];
|
|
65
|
+
const commonRoot = gitCommonWorkspaceRoot(primary);
|
|
66
|
+
if (commonRoot && commonRoot !== primary) candidates.push(commonRoot);
|
|
67
|
+
return candidates;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function countsFromRows(rows) {
|
|
71
|
+
const counts = {};
|
|
72
|
+
for (const r of rows) counts[r.status] = Number(r.n || 0);
|
|
73
|
+
return counts;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function taskCountRows(db, workspaceRoot) {
|
|
77
|
+
return workspaceRoot
|
|
78
|
+
? db.prepare('SELECT status, COUNT(*) n FROM tasks WHERE workspace_root = ? GROUP BY status').all(workspaceRoot)
|
|
79
|
+
: db.prepare('SELECT status, COUNT(*) n FROM tasks GROUP BY status').all();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function loadTaskCounts({ cwd, all = false } = {}) {
|
|
83
|
+
const candidates = all ? [null] : workspaceRootCandidates(cwd || process.cwd());
|
|
45
84
|
try {
|
|
46
85
|
const { DatabaseSync } = require('node:sqlite');
|
|
47
|
-
const
|
|
48
|
-
|
|
86
|
+
const dbPath = taskDb.getDbPath();
|
|
87
|
+
if (!fs.existsSync(dbPath)) return { counts: null, workspaceRoot: candidates[0] || null };
|
|
88
|
+
const db = new DatabaseSync(dbPath, { readOnly: true });
|
|
89
|
+
for (const workspaceRoot of candidates) {
|
|
90
|
+
const rows = taskCountRows(db, workspaceRoot);
|
|
91
|
+
if (all || rows.length > 0) {
|
|
92
|
+
db.close();
|
|
93
|
+
return { counts: countsFromRows(rows), workspaceRoot };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
49
96
|
db.close();
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
97
|
+
return { counts: {}, workspaceRoot: candidates[0] || null };
|
|
98
|
+
} catch { return { counts: null, workspaceRoot: candidates[0] || null }; }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function taskScopeLabel(scope) {
|
|
102
|
+
return scope.kind === 'global' ? 'global' : 'workspace';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function taskScopeLine(scope) {
|
|
106
|
+
if (scope.kind === 'global') return 'global';
|
|
107
|
+
return `${taskScopeLabel(scope)} (${scope.workspace_root})`;
|
|
54
108
|
}
|
|
55
109
|
|
|
56
110
|
function loadFeatures(cwd) {
|
|
@@ -110,9 +164,14 @@ function truthCommand(args = []) {
|
|
|
110
164
|
const cwd = process.cwd();
|
|
111
165
|
const json = args.includes('--json');
|
|
112
166
|
const summary = args.includes('--summary');
|
|
167
|
+
const all = args.includes('--all');
|
|
113
168
|
|
|
114
169
|
const missions = loadMissions(cwd);
|
|
115
|
-
const
|
|
170
|
+
const taskCounts = loadTaskCounts({ cwd, all });
|
|
171
|
+
const scope = all
|
|
172
|
+
? { kind: 'global' }
|
|
173
|
+
: { kind: 'workspace', workspace_root: taskCounts.workspaceRoot };
|
|
174
|
+
const tasks = taskCounts.counts;
|
|
116
175
|
const features = loadFeatures(cwd);
|
|
117
176
|
const heartbeats = loadHeartbeats();
|
|
118
177
|
|
|
@@ -124,6 +183,7 @@ function truthCommand(args = []) {
|
|
|
124
183
|
const result = {
|
|
125
184
|
ok: true,
|
|
126
185
|
generated_at: new Date().toISOString(),
|
|
186
|
+
scope,
|
|
127
187
|
missions_active: missions.map((m) => ({ id: m.id || m.mission_id, status: m.status, owner: m.owner })),
|
|
128
188
|
tasks,
|
|
129
189
|
features: featureTally,
|
|
@@ -139,11 +199,12 @@ function truthCommand(args = []) {
|
|
|
139
199
|
|
|
140
200
|
const line = (s) => console.log(s);
|
|
141
201
|
if (summary) {
|
|
142
|
-
line(`truth: features ${featureTally.proven || 0} proven / ${featureTally.stale || 0} stale / ${featureTally.blocked || 0} blocked / ${featureTally.unproven || 0} unproven · loops ${loopTally.proven || 0} live / ${(loopTally.stale || 0) + (loopTally['never-ran'] || 0)} stale / ${loopTally.blocked || 0} failing · missions ${missions.length} active`);
|
|
202
|
+
line(`truth [${taskScopeLabel(scope)}]: features ${featureTally.proven || 0} proven / ${featureTally.stale || 0} stale / ${featureTally.blocked || 0} blocked / ${featureTally.unproven || 0} unproven · loops ${loopTally.proven || 0} live / ${(loopTally.stale || 0) + (loopTally['never-ran'] || 0)} stale / ${loopTally.blocked || 0} failing · missions ${missions.length} active`);
|
|
143
203
|
return 0;
|
|
144
204
|
}
|
|
145
205
|
|
|
146
206
|
line('ATRIS TRUTH — live state, not belief\n');
|
|
207
|
+
line(`Scope: ${taskScopeLine(scope)}`);
|
|
147
208
|
|
|
148
209
|
line(`Missions active: ${missions.length}`);
|
|
149
210
|
for (const m of missions) line(` ${m.status.padEnd(8)} ${m.owner || '?'} ${m.id || m.mission_id}`);
|
package/lib/autoland.js
CHANGED
|
@@ -12,6 +12,8 @@ const { spawnSync } = require('child_process');
|
|
|
12
12
|
|
|
13
13
|
const DEFAULT_DIGEST_HOUR = 9;
|
|
14
14
|
const DEFAULT_ALARM_HOURS = 24;
|
|
15
|
+
const OPERATOR_WHY = /\b(sav(?:e|es|ing)|burn(?:s|ing)?|wast(?:e|es|ing)|cost(?:s|ing)?|prevent\w*|stop(?:s|ping)?|break(?:s|ing)?|fail(?:s|ing)?|slow(?:s|er)?|faster|cheaper|easier|clearer|safer|simpler|trust|revenue|users?|customers?|so that|because)\b/i;
|
|
16
|
+
const AGENT_JARGON = /[a-z0-9]_[a-z0-9]|\b[a-z]+[A-Z]\w*|--[a-z]|\bCLI-\d+/;
|
|
15
17
|
|
|
16
18
|
function policyPath(root) {
|
|
17
19
|
return path.join(root, '.atris', 'policy', 'autoland.json');
|
|
@@ -131,7 +133,7 @@ function waitingOnHuman(tasks, now = Date.now()) {
|
|
|
131
133
|
.filter((t) => t.review?.agent_certified === true || t.metadata?.agent_certified === true)
|
|
132
134
|
.map((t) => ({
|
|
133
135
|
ref: t.display_id || t.legacy_ref || t.id,
|
|
134
|
-
title: String(t.title || '').slice(0,
|
|
136
|
+
title: String(t.title || '').slice(0, 200),
|
|
135
137
|
tag: String(t.tag || ''),
|
|
136
138
|
hours: waitingHours(t, now),
|
|
137
139
|
}))
|
|
@@ -168,16 +170,54 @@ function cutAtWord(text, max) {
|
|
|
168
170
|
return `${cut.slice(0, lastSpace > max * 0.6 ? lastSpace : max)}...`;
|
|
169
171
|
}
|
|
170
172
|
|
|
173
|
+
// A line ends where a thought ends. Long lines close at their last complete
|
|
174
|
+
// clause inside the budget and read whole: no ellipsis, nothing dangling.
|
|
175
|
+
// Only when a line has no clause boundary at all does a word cut remain.
|
|
176
|
+
function finishThought(text, max = 160) {
|
|
177
|
+
const clean = String(text || '').replace(/\s+/g, ' ').trim().replace(/[,;:\s]+$/, '');
|
|
178
|
+
if (clean.length <= max) return clean;
|
|
179
|
+
const cut = clean.slice(0, max);
|
|
180
|
+
const sentence = Math.max(cut.lastIndexOf('. '), cut.lastIndexOf('! '), cut.lastIndexOf('? '));
|
|
181
|
+
if (sentence > max * 0.3) return cut.slice(0, sentence + 1);
|
|
182
|
+
const clause = Math.max(cut.lastIndexOf(', '), cut.lastIndexOf('; '), cut.lastIndexOf(' ('), cut.lastIndexOf(': '));
|
|
183
|
+
if (clause > max * 0.45) return cut.slice(0, clause);
|
|
184
|
+
const space = cut.lastIndexOf(' ');
|
|
185
|
+
return `${cut.slice(0, space > max * 0.6 ? space : max)}...`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function operatorReady(text) {
|
|
189
|
+
const value = String(text || '');
|
|
190
|
+
return OPERATOR_WHY.test(value) && !AGENT_JARGON.test(value);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function hasAgentJargon(text) {
|
|
194
|
+
return AGENT_JARGON.test(String(text || ''));
|
|
195
|
+
}
|
|
196
|
+
|
|
171
197
|
// One line per piece, in the work's own words: the landing sentence beats
|
|
172
198
|
// the task title, because titles are written for the queue, not for a text.
|
|
199
|
+
// The cut is generous on purpose: a specific sentence the reader trusts
|
|
200
|
+
// (what happened + how big + how we know) beats a short one they must chase.
|
|
173
201
|
function digestLine(item) {
|
|
174
|
-
const happened = String(item.happened || '').split(/(?<=[.!?])\s+/)[0] || '';
|
|
202
|
+
const happened = String(item.happened || '').replace(/^completed:\s*/i, '').split(/(?<=[.!?])\s+/)[0] || '';
|
|
175
203
|
const title = String(item.title || '').replace(/^Mission XP:\s*/i, '');
|
|
176
204
|
const source = happened.length >= 12 ? happened : title;
|
|
177
|
-
const line =
|
|
205
|
+
const line = finishThought(source.replace(/[.\s]+$/, ''), 160);
|
|
178
206
|
return line.charAt(0).toLowerCase() + line.slice(1);
|
|
179
207
|
}
|
|
180
208
|
|
|
209
|
+
// A rough sentence still beats a count: when a landing sentence carries agent
|
|
210
|
+
// vocabulary, strip what a reader can't use (flag dashes, ids, underscores)
|
|
211
|
+
// and show it anyway. Content always ships; the gate only changes how it reads.
|
|
212
|
+
function dejargon(line) {
|
|
213
|
+
return String(line || '')
|
|
214
|
+
.replace(/--([a-z][a-z-]*)/g, '$1')
|
|
215
|
+
.replace(/\bCLI-\d+\b/g, '')
|
|
216
|
+
.replace(/([a-z0-9])_([a-z0-9])/gi, '$1 $2')
|
|
217
|
+
.replace(/\s+/g, ' ')
|
|
218
|
+
.trim();
|
|
219
|
+
}
|
|
220
|
+
|
|
181
221
|
// The mission loop files a check-off every time it stops instead of
|
|
182
222
|
// inventing work; those all say the same thing, so they collapse into one line.
|
|
183
223
|
function isCleanStop(item) {
|
|
@@ -185,40 +225,105 @@ function isCleanStop(item) {
|
|
|
185
225
|
|| /^(Mission XP:\s*)?Decide and start the next useful mission/i.test(String(item.title || ''));
|
|
186
226
|
}
|
|
187
227
|
|
|
188
|
-
// Short enough for one iMessage, plain enough to read
|
|
189
|
-
//
|
|
190
|
-
|
|
228
|
+
// Short enough for one iMessage, plain enough to read walking to the car.
|
|
229
|
+
// Every line answers: what happened, how big, how we know. It names the work
|
|
230
|
+
// because "7 things landed" tells the score, not the story — and it always
|
|
231
|
+
// ends with what's waiting and what to do next, because those are the only
|
|
232
|
+
// lines that ask anything of the reader.
|
|
233
|
+
function composeDigest({ accepted, waiting, landed, project, nextMoves = [] }) {
|
|
191
234
|
const lines = [];
|
|
192
|
-
lines.push(`atris ${project}
|
|
235
|
+
lines.push(`atris ${project}: yesterday`);
|
|
193
236
|
const autoCount = accepted.auto.length;
|
|
194
237
|
const humanCount = accepted.human.length;
|
|
195
238
|
if (autoCount > 0) {
|
|
196
|
-
|
|
239
|
+
// Three results, air between them, the rest on ask: the whole message
|
|
240
|
+
// fits a laptop screen with no scrolling. Reading it is one glance.
|
|
197
241
|
const stops = accepted.auto.filter(isCleanStop);
|
|
198
|
-
const work = accepted.auto
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
242
|
+
const work = accepted.auto
|
|
243
|
+
.filter((item) => !isCleanStop(item))
|
|
244
|
+
.map((item) => ({ item, line: digestLine(item) }));
|
|
245
|
+
// Operator-ready sentences lead; rough ones get de-jargoned and still
|
|
246
|
+
// shown. An empty report is the one thing a report must never be.
|
|
247
|
+
const ready = work.filter(({ line }) => operatorReady(line));
|
|
248
|
+
const rough = work.filter(({ line }) => !operatorReady(line))
|
|
249
|
+
.map(({ item, line }) => ({ item, line: dejargon(line) }));
|
|
250
|
+
const visibleWork = [...ready, ...rough].slice(0, 3);
|
|
251
|
+
const held = Math.max(0, work.length - visibleWork.length) + (stops.length > 0 ? 1 : 0);
|
|
252
|
+
if (visibleWork.length > 0) {
|
|
253
|
+
lines.push('landed on their own (verified twice, proof on file):');
|
|
254
|
+
for (const { item, line } of visibleWork) {
|
|
255
|
+
lines.push('');
|
|
256
|
+
lines.push(`- ${line || item.ref}${item.member ? ` (${item.member})` : ''}`);
|
|
257
|
+
}
|
|
258
|
+
lines.push('');
|
|
259
|
+
if (held > 0) lines.push(`${plur(held, 'more result')} when you want them: atris autoland digest`);
|
|
260
|
+
}
|
|
202
261
|
}
|
|
203
|
-
if (humanCount > 0) lines.push(`you approved ${plur(humanCount, 'piece')} yourself`);
|
|
204
262
|
if (autoCount + humanCount === 0) lines.push('nothing finished in the last day');
|
|
205
|
-
const byMember = new Map();
|
|
206
|
-
for (const item of [...accepted.auto, ...accepted.human]) {
|
|
207
|
-
if (!item.member || isCleanStop(item)) continue;
|
|
208
|
-
byMember.set(item.member, (byMember.get(item.member) || 0) + 1);
|
|
209
|
-
}
|
|
210
|
-
if (byMember.size > 0) {
|
|
211
|
-
lines.push(`workers: ${Array.from(byMember, ([who, n]) => `${who} landed ${n}`).join(', ')}`);
|
|
212
|
-
}
|
|
213
263
|
if (waiting.length > 0) {
|
|
214
|
-
lines
|
|
264
|
+
// The only lines that ask the reader to act, so they name what's asking.
|
|
265
|
+
lines.push(`waiting on you (approve or bounce: atris task reviews):`);
|
|
266
|
+
for (const item of waiting.slice(0, 2)) {
|
|
267
|
+
lines.push(`- ${finishThought(dejargon(item.title), 140)}${item.hours >= 12 ? ` (${item.hours}h)` : ''}`);
|
|
268
|
+
}
|
|
269
|
+
if (waiting.length > 2) lines.push(`- and ${waiting.length - 2} more`);
|
|
215
270
|
} else {
|
|
216
|
-
lines.push('
|
|
271
|
+
lines.push('waiting on you: nothing');
|
|
217
272
|
}
|
|
218
273
|
if (landed && landed.branches > 0) {
|
|
219
|
-
lines.push(
|
|
274
|
+
lines.push(`in the air: ${plur(landed.branches, 'piece')}${landed.due > 0 ? `, ${landed.due} overdue` : ', all fresh'} (atris land)`);
|
|
275
|
+
}
|
|
276
|
+
// nextMoves: array of {title, owner}, or {moves, unexplained} where
|
|
277
|
+
// unexplained counts queue items whose sentence carries no operator why.
|
|
278
|
+
// Those are counted, not shown — a raw title the reader can't act on is
|
|
279
|
+
// noise here and a writing bug at its source.
|
|
280
|
+
const nextInfo = Array.isArray(nextMoves)
|
|
281
|
+
? { moves: nextMoves, unexplained: 0 }
|
|
282
|
+
: (nextMoves || { moves: [], unexplained: 0 });
|
|
283
|
+
const moves = (nextInfo.moves || []).filter((move) => move && move.title).slice(0, 3);
|
|
284
|
+
const unexplained = Number(nextInfo.unexplained || 0);
|
|
285
|
+
if (moves.length > 0) {
|
|
286
|
+
lines.push('next, if you agree:');
|
|
287
|
+
for (const move of moves) {
|
|
288
|
+
lines.push(`- ${finishThought(move.title, 140)}${move.owner ? ` (best fit: ${move.owner})` : ''}`);
|
|
289
|
+
}
|
|
290
|
+
if (unexplained > 0) lines.push(`- ${plur(unexplained, 'more idea')} that can't explain themselves yet (atris now)`);
|
|
291
|
+
} else if (unexplained > 0) {
|
|
292
|
+
lines.push(`${plur(unexplained, 'idea')} in the queue can't explain themselves yet (atris now)`);
|
|
293
|
+
}
|
|
294
|
+
if (!lines.some((line) => line.includes('atris autoland digest'))) {
|
|
295
|
+
lines.push('the full story: atris autoland digest');
|
|
296
|
+
}
|
|
297
|
+
return lines.join('\n');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// The moment something lands, the operator hears it in one glance: the
|
|
301
|
+
// landing sentence each piece wrote for a day-one PM at finish time, its
|
|
302
|
+
// author, nothing else. Rough sentences get de-jargoned, never hidden.
|
|
303
|
+
function composeLiveUpdate({ landedRefs, tasks, project }) {
|
|
304
|
+
const byRef = new Map((tasks || []).map((t) => [String(t.display_id || t.legacy_ref || t.id), t]));
|
|
305
|
+
const items = (landedRefs || []).map((ref) => {
|
|
306
|
+
const t = byRef.get(String(ref));
|
|
307
|
+
if (!t) return { title: String(ref) };
|
|
308
|
+
return {
|
|
309
|
+
ref: String(ref),
|
|
310
|
+
title: String(t.title || '').slice(0, 200),
|
|
311
|
+
happened: String(t.review?.landing?.happened || t.metadata?.landing_happened || '').slice(0, 300),
|
|
312
|
+
member: String(t.claimed_by || t.assigned_to || '').trim(),
|
|
313
|
+
};
|
|
314
|
+
}).filter((item) => !isCleanStop(item));
|
|
315
|
+
if (!items.length) return '';
|
|
316
|
+
const lines = [`atris ${project}: just landed`];
|
|
317
|
+
for (const item of items.slice(0, 3)) {
|
|
318
|
+
const raw = digestLine(item);
|
|
319
|
+
const line = operatorReady(raw) ? raw : dejargon(raw);
|
|
320
|
+
lines.push('');
|
|
321
|
+
lines.push(`- ${finishThought(line, 160)}${item.member ? ` (${item.member})` : ''}`);
|
|
322
|
+
}
|
|
323
|
+
if (items.length > 3) {
|
|
324
|
+
lines.push('');
|
|
325
|
+
lines.push(`and ${plur(items.length - 3, 'more piece')}: atris autoland digest`);
|
|
220
326
|
}
|
|
221
|
-
lines.push('the full story: atris autoland digest');
|
|
222
327
|
return lines.join('\n');
|
|
223
328
|
}
|
|
224
329
|
|
|
@@ -264,12 +369,15 @@ module.exports = {
|
|
|
264
369
|
buildCronLine,
|
|
265
370
|
composeAlarm,
|
|
266
371
|
composeDigest,
|
|
372
|
+
composeLiveUpdate,
|
|
267
373
|
cronInstalled,
|
|
268
374
|
cronMarker,
|
|
269
375
|
dueForAlarm,
|
|
270
376
|
installCron,
|
|
271
377
|
liveAcceptAuthorization,
|
|
272
378
|
markAlerted,
|
|
379
|
+
operatorReady,
|
|
380
|
+
hasAgentJargon,
|
|
273
381
|
policyPath,
|
|
274
382
|
readPolicy,
|
|
275
383
|
readState,
|