atris 3.82.3 → 3.82.4
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 +1 -1
- package/atris/atris.md +6 -0
- package/bin/atris.js +1 -0
- package/commands/aeo.js +15 -2
- package/commands/next.js +16 -6
- package/commands/now.js +6 -1
- package/commands/security-review.js +24 -0
- package/commands/sentinel-powers.js +284 -0
- package/commands/task.js +9 -2
- package/lib/agent-next.js +255 -0
- package/lib/first-minute.js +8 -4
- package/lib/member-context.js +11 -2
- package/lib/sentinel-agents.js +584 -0
- package/lib/sentinel-cloud.js +3 -0
- package/lib/sentinel-events.js +320 -0
- package/lib/sentinel-guard-install.js +163 -0
- package/lib/sentinel-guard.js +856 -0
- package/lib/sentinel-watch.js +529 -0
- package/lib/task-core.js +3 -3
- package/lib/task-db.js +12 -3
- package/package.json +1 -1
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// A read-only recommendation. Existing task commands own every transition.
|
|
4
|
+
const { readFlag } = require('./arg-parser');
|
|
5
|
+
const { workerEnvMarker } = require('./task-core');
|
|
6
|
+
const { autoJobForMember } = require('./member-engine');
|
|
7
|
+
const { skipReason, heldForPerson, taskCheck } = require('./member-task-pick');
|
|
8
|
+
const { buildNextWorkContext } = require('./next-work-context');
|
|
9
|
+
const reviewIntegrity = require('./review-integrity');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
|
|
12
|
+
const text = (value) => typeof value === 'string' ? value.trim() : '';
|
|
13
|
+
const quote = (value) => /^[A-Za-z0-9_.:-]+$/.test(String(value))
|
|
14
|
+
? String(value) : "'" + String(value).replace(/'/g, "'\\''") + "'";
|
|
15
|
+
const identifier = (value) => typeof value === 'string' && /^[A-Za-z0-9_.:-]{1,120}$/.test(value) ? value : null;
|
|
16
|
+
const ref = (task) => identifier(task?.display_id) || identifier(task?.id) || '';
|
|
17
|
+
const certified = (task) => task?.review?.agent_certified === true || task?.metadata?.agent_certified === true;
|
|
18
|
+
const ownerField = (value) => typeof value === 'string' && value.length <= 80 && !/[\u0000-\u001f]/.test(value) ? value || null : null;
|
|
19
|
+
const knownStatus = (value) => ['open', 'claimed', 'review', 'done', 'failed', 'archived', 'blocked'].includes(value) ? value : null;
|
|
20
|
+
const sourceKind = (value) => ['database', 'mixed', 'projection', 'none'].includes(value) ? value : 'none';
|
|
21
|
+
|
|
22
|
+
function agentCaller(args = [], env = process.env) {
|
|
23
|
+
if (args.includes('--human')) return null;
|
|
24
|
+
const explicit = args.some((arg) => arg === '--agent' || arg.startsWith('--agent='));
|
|
25
|
+
if (!explicit && env.ATRIS_AGENT_PROOF_ONLY === '0') return null;
|
|
26
|
+
if (!explicit && !workerEnvMarker(env) && !text(env.ATRIS_AGENT_ID)) return null;
|
|
27
|
+
const named = text(readFlag(args, '--agent'));
|
|
28
|
+
const as = text(readFlag(args, '--as'));
|
|
29
|
+
const member = named || as || text(env.ATRIS_AGENT_ID) || 'builder';
|
|
30
|
+
const error = named && as && named !== as ? 'conflicting_owner'
|
|
31
|
+
: !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,79}$/.test(member) ? 'invalid_owner' : null;
|
|
32
|
+
return { kind: 'agent', member, error };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function checkFor(task) {
|
|
36
|
+
const metadata = task?.metadata || {};
|
|
37
|
+
const command = text(metadata.verify) || text(metadata.proof_needed);
|
|
38
|
+
return {
|
|
39
|
+
command,
|
|
40
|
+
runnable: Boolean(command) && command.length <= 600
|
|
41
|
+
&& taskCheck({ metadata: { verify: command } }).runnable,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function planningNeeds(task) {
|
|
46
|
+
const meta = task.metadata || {};
|
|
47
|
+
const needs = [];
|
|
48
|
+
if (!checkFor(task).runnable) needs.push('runnable_check');
|
|
49
|
+
if (!(text(meta.task_goal) || text(meta.goal_objective) || text(meta.objective) || text(meta.stage_goal))) needs.push('goal');
|
|
50
|
+
if (!(text(meta.exit_condition) || text(meta.done_looks_like))) needs.push('exit_condition');
|
|
51
|
+
if (!text(meta.first_move)) needs.push('first_move');
|
|
52
|
+
if (!(meta.planned_at || meta.stage_plan_recorded_at || meta.stage === 'plan' || meta.stage === 'do')) needs.push('recorded_plan');
|
|
53
|
+
return needs;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function recommendAgentWork({ tasks = [], member, job = 'build', taskSource = 'database', taskId = '', isAuthor = reviewIntegrity.isTaskAuthor } = {}) {
|
|
57
|
+
const pending = tasks.filter((task) => taskSource === 'database' && identifier(task?.id)
|
|
58
|
+
&& task?.status === 'review' && certified(task))
|
|
59
|
+
.sort((a, b) => Number(b.updated_at || 0) - Number(a.updated_at || 0))
|
|
60
|
+
.slice(0, 5).map((task) => ({
|
|
61
|
+
task_id: task.id || null, reference: ref(task) || null,
|
|
62
|
+
reason: 'human_acceptance', human_only: true,
|
|
63
|
+
command: ref(task) ? 'atris task accept ' + quote(ref(task)) : null,
|
|
64
|
+
}));
|
|
65
|
+
const result = (task, action, reason, needs = []) => ({
|
|
66
|
+
task, action, reason, needs, human_pending: pending,
|
|
67
|
+
});
|
|
68
|
+
const bound = taskId ? tasks.find((task) => task.id === taskId || ref(task) === taskId) : null;
|
|
69
|
+
if (taskId && !bound) return result(null, 'blocked', 'The requested task is not in current workspace evidence.', ['current_task']);
|
|
70
|
+
if (bound && taskSource !== 'database') {
|
|
71
|
+
return result(bound, 'discover', 'Confirm the bound task state before relying on this snapshot.', ['authoritative_task_state']);
|
|
72
|
+
}
|
|
73
|
+
const rows = taskId ? [bound] : tasks;
|
|
74
|
+
const eligible = rows.filter((task) => {
|
|
75
|
+
if (!task || typeof task !== 'object') return false;
|
|
76
|
+
// An absent or unusable check still permits planning. It never permits
|
|
77
|
+
// claiming or executing; ownership and human holds remain unchanged.
|
|
78
|
+
const probe = { ...task, metadata: { ...task.metadata, verify: '' } };
|
|
79
|
+
return !skipReason(probe, job, { member, recoverClaimed: true });
|
|
80
|
+
}).sort((a, b) => Number(b.status === 'claimed') - Number(a.status === 'claimed')
|
|
81
|
+
|| Number(Boolean(text(b.metadata?.assigned_to))) - Number(Boolean(text(a.metadata?.assigned_to)))
|
|
82
|
+
|| Number(a.created_at || 0) - Number(b.created_at || 0)
|
|
83
|
+
|| String(a.id || '').localeCompare(String(b.id || '')));
|
|
84
|
+
let selected = eligible[0] || null;
|
|
85
|
+
if (!selected && job === 'review') {
|
|
86
|
+
selected = rows.find((task) => task?.status === 'review' && !certified(task)
|
|
87
|
+
&& !heldForPerson(task) && !isAuthor(task, member)) || null;
|
|
88
|
+
if (selected) {
|
|
89
|
+
if (taskSource !== 'database') return result(selected, 'discover', 'Confirm the current task state before reviewing this snapshot.', ['authoritative_task_state']);
|
|
90
|
+
return result(selected, 'review', 'An independent reviewer can check the finished work.');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!selected) {
|
|
94
|
+
if (bound) return result(bound, 'blocked', 'This member has no permitted step on the requested task. Inspect its current contract.', ['eligible_task']);
|
|
95
|
+
return result(null, 'discover', 'No eligible work is recorded. Inspect the goal and prior results before proposing one verifiable change.', ['eligible_task']);
|
|
96
|
+
}
|
|
97
|
+
if (taskSource !== 'database') {
|
|
98
|
+
return result(selected, 'discover', 'Confirm current task evidence before claiming or changing this snapshot.', ['authoritative_task_state']);
|
|
99
|
+
}
|
|
100
|
+
const needs = planningNeeds(selected);
|
|
101
|
+
if (needs.length) return result(selected, 'plan', 'Inspect this task and record its goal, exit, first move and runnable check before starting it.', needs);
|
|
102
|
+
return selected.status === 'claimed'
|
|
103
|
+
? result(selected, 'continue', 'Continue the owned work and collect real proof before marking it ready.')
|
|
104
|
+
: result(selected, 'claim', 'This owned or matching task has a recorded plan and runnable check.');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function recommendationCommand(decision, member) {
|
|
108
|
+
if (decision.action === 'discover') return null;
|
|
109
|
+
const taskRef = ref(decision.task);
|
|
110
|
+
if (!taskRef) return null;
|
|
111
|
+
if (decision.action === 'claim') return 'atris task claim ' + quote(taskRef) + ' --as ' + quote(member);
|
|
112
|
+
if (decision.action === 'review') return 'atris task review-chat ' + quote(taskRef) + ' --as ' + quote(member);
|
|
113
|
+
return 'atris task page ' + quote(taskRef) + ' --json';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function boundedPacket(payload) {
|
|
117
|
+
payload.output_budget = { max_bytes: 16000, limited: false };
|
|
118
|
+
// Include the longest alias, pretty formatting and the CLI newline.
|
|
119
|
+
const bytes = () => Buffer.byteLength(JSON.stringify({ ...payload, next_action: payload.command }, null, 2) + '\n');
|
|
120
|
+
const limited = () => {
|
|
121
|
+
payload.output_budget.limited = true;
|
|
122
|
+
if (Array.isArray(payload.context.gaps) && !payload.context.gaps.includes('response_budget_limited')) {
|
|
123
|
+
payload.context.gaps.push('response_budget_limited');
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
while (bytes() > 16000 && payload.human_pending.length > 1) {
|
|
127
|
+
payload.human_pending.pop();
|
|
128
|
+
limited();
|
|
129
|
+
}
|
|
130
|
+
for (const key of ['candidates', 'recent_outcomes', 'parts']) {
|
|
131
|
+
while (bytes() > 16000 && Array.isArray(payload.context[key]) && payload.context[key].length) {
|
|
132
|
+
payload.context[key].pop();
|
|
133
|
+
limited();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (bytes() > 16000) {
|
|
137
|
+
limited();
|
|
138
|
+
payload.action = payload.action === 'blocked' ? 'blocked' : 'discover';
|
|
139
|
+
payload.command = null;
|
|
140
|
+
payload.scope = {};
|
|
141
|
+
payload.reason = 'The evidence exceeds the response budget. Inspect current sources before acting.';
|
|
142
|
+
payload.needs = [...new Set([...payload.needs, 'bounded_evidence'])];
|
|
143
|
+
payload.human_pending = [];
|
|
144
|
+
payload.context = {
|
|
145
|
+
schema: 'atris.next_work_context.v1', task_source: sourceKind(payload.context.task_source),
|
|
146
|
+
goal: null, parts: [], candidates: [], recent_outcomes: [], sources: [],
|
|
147
|
+
gaps: ['response_budget_limited'], next_step: { kind: 'discover', reason: payload.reason, check: null },
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return payload;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function buildAgentNext({ root = process.cwd(), args = [], env = process.env, snapshot, tasks, scope = {}, evidence } = {}) {
|
|
154
|
+
const caller = agentCaller(args, env);
|
|
155
|
+
if (!caller) return null;
|
|
156
|
+
const taskDb = require('./task-db');
|
|
157
|
+
const workspaceRoot = taskDb.workspaceRoot(root);
|
|
158
|
+
const taskId = text(readFlag(args, '--task'));
|
|
159
|
+
const state = snapshot || require('./first-minute').loadLocalTaskState(workspaceRoot, {
|
|
160
|
+
includeAllActive: true, taskIds: taskId ? [taskId] : [],
|
|
161
|
+
});
|
|
162
|
+
const taskSource = sourceKind(state.source);
|
|
163
|
+
let rows = tasks || state.tasks;
|
|
164
|
+
let queryScope = scope;
|
|
165
|
+
if (!tasks && args.some((arg) => /^--(?:goal[-_]id|tag|status|review[-_]state)(?:=|$)/.test(arg))) {
|
|
166
|
+
// Reuse the task surface's existing filters, including goal/review scope.
|
|
167
|
+
// Loaded only for scoped calls, after command modules have initialized.
|
|
168
|
+
const { taskQueueScopeFromArgs, normalizeTaskQueueScope, filterTasksByScope } = require('../commands/task');
|
|
169
|
+
const queryArgs = args.flatMap((arg) => /^--[^=]+=/.test(arg)
|
|
170
|
+
? [arg.slice(0, arg.indexOf('=')), arg.slice(arg.indexOf('=') + 1)] : [arg]);
|
|
171
|
+
queryScope = normalizeTaskQueueScope(taskQueueScopeFromArgs(queryArgs));
|
|
172
|
+
rows = filterTasksByScope(rows, queryScope);
|
|
173
|
+
}
|
|
174
|
+
const scopeFits = Buffer.byteLength(JSON.stringify(queryScope)) <= 2048;
|
|
175
|
+
const job = caller.error ? 'build' : autoJobForMember(caller.member, '');
|
|
176
|
+
let authorshipAvailable = true;
|
|
177
|
+
const isAuthor = job === 'review' && taskSource === 'database' ? (task, actor) => {
|
|
178
|
+
try {
|
|
179
|
+
if (!fs.existsSync(taskDb.getDbPath())) throw new Error('task history unavailable');
|
|
180
|
+
return taskDb.isAuthorOfTask(taskDb.open(), task, actor);
|
|
181
|
+
} catch {
|
|
182
|
+
authorshipAvailable = false;
|
|
183
|
+
return true; // Unknown authorship cannot establish an independent reviewer.
|
|
184
|
+
}
|
|
185
|
+
} : reviewIntegrity.isTaskAuthor;
|
|
186
|
+
const decision = caller.error
|
|
187
|
+
? { task: null, action: 'blocked', reason: 'Name one valid functional owner for this agent.', needs: ['functional_owner'], human_pending: [] }
|
|
188
|
+
: !scopeFits ? { task: null, action: 'blocked', reason: 'Keep the requested scope small enough to inspect.', needs: ['bounded_scope'], human_pending: [] }
|
|
189
|
+
: recommendAgentWork({ tasks: rows, member: caller.member, job, taskSource, taskId, isAuthor });
|
|
190
|
+
if (!authorshipAvailable && decision.action !== 'review') {
|
|
191
|
+
decision.needs.push('authorship_history');
|
|
192
|
+
decision.reason = 'Confirm the task authorship history before assigning an independent review.';
|
|
193
|
+
}
|
|
194
|
+
if (decision.task && !identifier(decision.task.id)) {
|
|
195
|
+
decision.action = 'discover';
|
|
196
|
+
decision.needs.push('stable_task_identity');
|
|
197
|
+
decision.reason = 'Confirm a stable task identity before using this evidence.';
|
|
198
|
+
}
|
|
199
|
+
if (decision.task && [decision.task.claimed_by, decision.task.metadata?.assigned_to]
|
|
200
|
+
.some((value) => value != null && value !== '' && ownerField(value) === null)) {
|
|
201
|
+
decision.action = 'discover';
|
|
202
|
+
decision.needs.push('valid_task_ownership');
|
|
203
|
+
decision.reason = 'Confirm the recorded ownership before relying on this task evidence.';
|
|
204
|
+
}
|
|
205
|
+
if (decision.task && !knownStatus(decision.task.status)) {
|
|
206
|
+
decision.action = 'discover';
|
|
207
|
+
decision.needs.push('current_task_status');
|
|
208
|
+
}
|
|
209
|
+
const command = recommendationCommand(decision, caller.member);
|
|
210
|
+
const context = evidence || buildNextWorkContext({
|
|
211
|
+
root: workspaceRoot, tasks: rows, selected: decision.task, owner: caller.member,
|
|
212
|
+
taskSource, generatedAt: state.generated_at,
|
|
213
|
+
});
|
|
214
|
+
context.task_source = taskSource;
|
|
215
|
+
// The existing evidence packet keeps its own factual gaps. The action is
|
|
216
|
+
// the owner's next permitted step, not a claim that a check has passed.
|
|
217
|
+
context.next_step.kind = decision.action === 'plan' ? 'discover'
|
|
218
|
+
: ['claim', 'continue'].includes(decision.action) ? 'execute'
|
|
219
|
+
: decision.action === 'review' ? 'review' : decision.action === 'discover' ? 'discover'
|
|
220
|
+
: ['human_decision', 'wait'].includes(context.next_step.kind) ? context.next_step.kind : 'discover';
|
|
221
|
+
context.next_step.reason = decision.reason;
|
|
222
|
+
const task = decision.task;
|
|
223
|
+
return boundedPacket({
|
|
224
|
+
schema: 'atris.agent_next.v1', ok: !caller.error && scopeFits, status: decision.action === 'blocked' ? 'blocked' : 'ok',
|
|
225
|
+
actor: { kind: 'agent', member: caller.error ? null : caller.member },
|
|
226
|
+
owner: caller.error ? null : caller.member, action: decision.action,
|
|
227
|
+
task_id: identifier(task?.id), reference: task ? ref(task) || null : null,
|
|
228
|
+
command, reason: decision.reason, needs: decision.needs,
|
|
229
|
+
task: task ? {
|
|
230
|
+
id: identifier(task.id), display_id: identifier(task.display_id),
|
|
231
|
+
title: text(task.title).slice(0, 240), status: knownStatus(task.status),
|
|
232
|
+
claimed_by: ownerField(task.claimed_by), assigned_to: ownerField(task.metadata?.assigned_to),
|
|
233
|
+
} : null,
|
|
234
|
+
human_pending: decision.human_pending, context, scope: scopeFits ? queryScope : {},
|
|
235
|
+
safety: { read_only: true, claims_work: false, human_accept: false, awards_xp: false },
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function printAgentNext(payload, { asJson = false, alias = 'next_action', log = console.log } = {}) {
|
|
240
|
+
if (asJson) {
|
|
241
|
+
log(JSON.stringify({ ...payload, [alias]: payload.command }, null, 2));
|
|
242
|
+
return 0;
|
|
243
|
+
}
|
|
244
|
+
const title = payload.task?.title;
|
|
245
|
+
const message = payload.action === 'plan' && title ? 'plan ' + title + ' and define its check.'
|
|
246
|
+
: payload.action === 'claim' && title ? title + ' has a plan and a check.'
|
|
247
|
+
: payload.action === 'continue' && title ? 'continue ' + title + '.'
|
|
248
|
+
: payload.reason;
|
|
249
|
+
log(message.replace(/^[A-Z]/, (letter) => letter.toLowerCase()));
|
|
250
|
+
if (payload.human_pending.length) log('\nhuman approval is still pending; this step does not accept that work.');
|
|
251
|
+
if (payload.command) log('\nnext: ' + payload.command);
|
|
252
|
+
return 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
module.exports = { agentCaller, planningNeeds, recommendAgentWork, buildAgentNext, printAgentNext };
|
package/lib/first-minute.js
CHANGED
|
@@ -177,7 +177,7 @@ function titlesFromTodoSection(root, section) {
|
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
function loadLocalTaskState(root = process.cwd(), { taskIds = [] } = {}) {
|
|
180
|
+
function loadLocalTaskState(root = process.cwd(), { taskIds = [], includeAllActive = false } = {}) {
|
|
181
181
|
let dbRows = [];
|
|
182
182
|
let lookup = () => null;
|
|
183
183
|
let databaseRead = false;
|
|
@@ -187,10 +187,12 @@ function loadLocalTaskState(root = process.cwd(), { taskIds = [] } = {}) {
|
|
|
187
187
|
if (!fs.existsSync(taskDb.getDbPath())) throw new Error('no task db yet');
|
|
188
188
|
const db = taskDb.open();
|
|
189
189
|
const workspaceRoot = taskDb.workspaceRoot(root);
|
|
190
|
-
const rows = taskDb.listTasks(db, { workspaceRoot, limit: 200 });
|
|
191
190
|
// The visible window must not become a new numbering universe. Task
|
|
192
191
|
// commands resolve these references against the complete workspace.
|
|
193
192
|
const refRows = taskDb.listTasks(db, { workspaceRoot });
|
|
193
|
+
const rows = includeAllActive
|
|
194
|
+
? refRows.filter((row) => ['open', 'claimed', 'review'].includes(row.status))
|
|
195
|
+
: taskDb.listTasks(db, { workspaceRoot, limit: 200 });
|
|
194
196
|
// Busy queues must not crowd the last outcomes out of the handoff.
|
|
195
197
|
const recent = ['done', 'failed'].flatMap((status) => taskDb.listTasks(db, { workspaceRoot, status, limit: 3, orderBy: 'updated_at' }));
|
|
196
198
|
const seen = new Set(rows.map((row) => row.id));
|
|
@@ -205,8 +207,10 @@ function loadLocalTaskState(root = process.cwd(), { taskIds = [] } = {}) {
|
|
|
205
207
|
};
|
|
206
208
|
// A shortlist is an output budget, not authority to invent a bound job.
|
|
207
209
|
for (const id of taskIds.slice(0, 8)) {
|
|
208
|
-
if (dbRows.some((row) => row.id === id)) continue;
|
|
209
|
-
const row = lookup(id)
|
|
210
|
+
if (dbRows.some((row) => row.id === id || (includeAllActive && row.display_id === id))) continue;
|
|
211
|
+
const row = lookup(id) || (includeAllActive
|
|
212
|
+
? taskDb.withTaskDisplayRefs(refRows, refRows).find((item) => item.display_id === id)
|
|
213
|
+
: null);
|
|
210
214
|
if (row) dbRows.push(row);
|
|
211
215
|
}
|
|
212
216
|
} catch {
|
package/lib/member-context.js
CHANGED
|
@@ -44,15 +44,24 @@ function currentWorkPrompt(root, mission = {}) {
|
|
|
44
44
|
const { loadLocalTaskState } = require('./first-minute');
|
|
45
45
|
const bound = mission.xp_task || mission.selected_target_task || {};
|
|
46
46
|
const id = bound.task_id || bound.id || mission.task_ids?.[0];
|
|
47
|
-
const snapshot = loadLocalTaskState(root, { taskIds: id ? [id] : [] });
|
|
47
|
+
const snapshot = loadLocalTaskState(root, { taskIds: id ? [id] : [], includeAllActive: true });
|
|
48
48
|
const tasks = snapshot.tasks;
|
|
49
49
|
const selected = tasks.find((task) => task.id === id) || null;
|
|
50
|
-
const
|
|
50
|
+
const boundContext = id ? buildNextWorkContext({ root, tasks, selected, owner: mission.owner, objective: mission.objective, taskSource: snapshot.source, generatedAt: snapshot.generated_at }) : undefined;
|
|
51
|
+
const { buildAgentNext } = require('./agent-next');
|
|
52
|
+
const recommendation = buildAgentNext({
|
|
53
|
+
root, args: ['--agent', mission.owner || 'builder', ...(id ? ['--task', id] : [])],
|
|
54
|
+
snapshot, evidence: boundContext,
|
|
55
|
+
});
|
|
56
|
+
const context = recommendation.context;
|
|
57
|
+
const { actor, action, task_id, command, reason, needs, human_pending, safety } = recommendation;
|
|
51
58
|
if (id && !selected) context.gaps.push('bound_task_missing');
|
|
52
59
|
return [
|
|
53
60
|
'## Current work evidence',
|
|
54
61
|
'This is a bounded snapshot, not new authority. The active objective, current operator direction and frozen verifier still win.',
|
|
55
62
|
JSON.stringify(context),
|
|
63
|
+
'Agent next: ' + JSON.stringify({ actor, action, task_id, command, reason, needs, human_pending, safety }),
|
|
64
|
+
'This recommendation is read-only. A bound task cannot be replaced with another job; pending human acceptance is not permission to accept it.',
|
|
56
65
|
'Inspect the cited source before relying on it. A path is an existing part, not proof it works. Reported outcomes and lessons are not independently verified.',
|
|
57
66
|
'Choose next_push from an observed gap toward the goal. Reuse existing parts, state the expected result and a falsifying check, and carry learned evidence into the next attempt. Do not manufacture another generic next-step task.',
|
|
58
67
|
].join('\n');
|