atris 3.34.0 → 3.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +35 -0
- package/FOR_AGENTS.md +5 -3
- package/README.md +5 -3
- package/atris/GETTING_STARTED.md +1 -1
- package/atris/atris.md +3 -0
- package/atris/policies/day-loop-voice.md +102 -0
- package/atris/policies/outbound-artifact-gate.md +2 -0
- package/atris/skills/design/SKILL.md +56 -32
- package/atris/skills/endgame/SKILL.md +12 -6
- package/atris/skills/engines/SKILL.md +35 -8
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/render-cli/SKILL.md +88 -0
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +6 -2
- package/ax +617 -31
- package/bin/atris.js +719 -266
- package/commands/activate.js +194 -88
- package/commands/aeo.js +52 -0
- package/commands/agents.js +166 -0
- package/commands/autoland.js +718 -72
- package/commands/autopilot-front.js +20 -2
- package/commands/autopilot.js +118 -2
- package/commands/avail.js +407 -0
- package/commands/bench.js +188 -0
- package/commands/brain.js +3 -0
- package/commands/brief.js +651 -0
- package/commands/business-sync.js +192 -6
- package/commands/business.js +91 -8
- package/commands/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- package/commands/codex-goal.js +26 -2
- package/commands/compile.js +292 -1
- package/commands/computer.js +150 -3
- package/commands/dream.js +365 -0
- package/commands/drill.js +371 -0
- package/commands/drive.js +187 -0
- package/commands/engine.js +1061 -29
- package/commands/experiments.js +28 -0
- package/commands/feed.js +202 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/github.js +38 -0
- package/commands/gm.js +285 -3
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +83 -43
- package/commands/integrations.js +39 -11
- package/commands/interview.js +209 -0
- package/commands/land.js +253 -52
- package/commands/lesson.js +112 -1
- package/commands/lifecycle.js +39 -3
- package/commands/linear.js +38 -0
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +899 -66
- package/commands/mission.js +3512 -332
- package/commands/next.js +137 -0
- package/commands/now.js +240 -21
- package/commands/one-lap.js +776 -0
- package/commands/orb.js +314 -0
- package/commands/pack-craft.js +179 -0
- package/commands/pack.js +823 -0
- package/commands/play.js +3 -2
- package/commands/probe.js +30 -3
- package/commands/pulse.js +241 -46
- package/commands/push.js +260 -82
- package/commands/radar.js +259 -14
- package/commands/rainmaker.js +49 -0
- package/commands/report.js +415 -0
- package/commands/scout.js +147 -0
- package/commands/search.js +363 -0
- package/commands/serve.js +54 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/status.js +50 -5
- package/commands/stream.js +861 -0
- package/commands/stripe.js +38 -0
- package/commands/study.js +693 -0
- package/commands/supabase.js +39 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +2275 -182
- package/commands/team.js +73 -0
- package/commands/truth.js +29 -3
- package/commands/unknowns.js +627 -0
- package/commands/update.js +44 -0
- package/commands/vercel.js +38 -0
- package/commands/verify.js +96 -0
- package/commands/watch.js +303 -0
- package/commands/wish.js +500 -0
- package/commands/workflow.js +11 -5
- package/commands/worktree.js +299 -20
- package/commands/write.js +399 -0
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +391 -43
- package/lib/autoland.js +353 -52
- package/lib/ax-auto-lane.js +79 -0
- package/lib/bench/context.js +147 -0
- package/lib/bench/engines.js +141 -0
- package/lib/bench/report.js +140 -0
- package/lib/bench/runner.js +512 -0
- package/lib/brief-ledger.js +350 -0
- package/lib/cloud-mission.js +259 -0
- package/lib/codex-flight.js +154 -0
- package/lib/default-runner.js +45 -0
- package/lib/default-verifier.js +70 -0
- package/lib/engine-registry.js +232 -0
- package/lib/experiments/daily.js +640 -0
- package/lib/fleet.js +2431 -38
- package/lib/improve-vitals-html.js +171 -0
- package/lib/known-commands.js +58 -0
- package/lib/loop-doctor.js +416 -0
- package/lib/member-switches.js +144 -0
- package/lib/memory-view.js +14 -5
- package/lib/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/mission-runtime-loop.js +7 -0
- package/lib/next-moves.js +327 -10
- package/lib/official-cli-integration.js +174 -0
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -0
- package/lib/outbound-send-gate.js +165 -0
- package/lib/permission-grants.js +293 -0
- package/lib/policy-lessons.js +52 -1
- package/lib/pulse.js +277 -3
- package/lib/receipt-block.js +168 -0
- package/lib/receipt-evidence.js +65 -4
- package/lib/review-integrity.js +147 -0
- package/lib/router-brain.js +352 -0
- package/lib/runner-command.js +33 -0
- package/lib/self-drive.js +258 -0
- package/lib/short-name.js +103 -0
- package/lib/spawn-env.js +18 -0
- package/lib/state-detection.js +56 -1
- package/lib/sync-status.js +59 -0
- package/lib/task-db.js +319 -27
- package/lib/task-proof.js +43 -1
- package/lib/task-receipt.js +93 -0
- package/lib/team-presence.js +260 -0
- package/lib/tool-result-encode.js +7 -0
- package/lib/trust-tiers.js +90 -0
- package/lib/usage.js +107 -0
- package/lib/voice-gate.js +163 -0
- package/lib/wish-audit.js +1368 -0
- package/lib/wish-delegate.js +1840 -0
- package/lib/wish-design.js +110 -0
- package/lib/wish-stats.js +183 -0
- package/lib/wish-store.js +354 -0
- package/lib/zip.js +221 -0
- package/package.json +3 -1
- package/templates/loops/atris/loops/LOOPS.md +55 -0
- package/templates/loops/atris/loops/TICK.md +24 -0
- package/templates/loops/atris/loops/feedback.md +22 -0
- package/templates/loops/atris/loops/quality.md +22 -0
- package/templates/loops/atris/wiki/systems/loops.md +41 -0
- package/utils/api.js +5 -1
- package/utils/auth.js +57 -21
- package/utils/update-check.js +27 -6
- package/atris/learnings.jsonl +0 -1
|
@@ -0,0 +1,1840 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { getLogPath, ensureLogDirectory, createLogFile, addInboxIdea } = require('./file-ops');
|
|
6
|
+
const { resolveFunctionalOwner } = require('./functional-owner');
|
|
7
|
+
const { buildMissionRoom, writeMissionRoomReceipt } = require('./mission-room');
|
|
8
|
+
const {
|
|
9
|
+
resolveDefaultVerifier,
|
|
10
|
+
verifierBudgetWarning,
|
|
11
|
+
} = require('./default-verifier');
|
|
12
|
+
const { detectDesignContext, isFrontendWish } = require('./wish-design');
|
|
13
|
+
const { delegateTask } = require('../commands/task');
|
|
14
|
+
const {
|
|
15
|
+
startMission,
|
|
16
|
+
spawnMissionDriver,
|
|
17
|
+
missionRunnerNeedsLiveSession,
|
|
18
|
+
listMissions,
|
|
19
|
+
listWorktreeRollupMissions,
|
|
20
|
+
pingMission,
|
|
21
|
+
findActiveTwinMission,
|
|
22
|
+
TWIN_ACTIVE_STATUSES,
|
|
23
|
+
} = require('../commands/mission');
|
|
24
|
+
const {
|
|
25
|
+
appendWishRecord,
|
|
26
|
+
eventAnswers,
|
|
27
|
+
improveWishes,
|
|
28
|
+
latestWishEventMap,
|
|
29
|
+
nextWishNumber,
|
|
30
|
+
readJsonLines,
|
|
31
|
+
readWishEvents,
|
|
32
|
+
readWishes,
|
|
33
|
+
stampIso,
|
|
34
|
+
wishId,
|
|
35
|
+
wishLessonsBrief,
|
|
36
|
+
} = require('./wish-store');
|
|
37
|
+
const {
|
|
38
|
+
recordMatchesRef,
|
|
39
|
+
shortRecordLabel,
|
|
40
|
+
shortRecordRef,
|
|
41
|
+
} = require('./short-name');
|
|
42
|
+
const {
|
|
43
|
+
WAITING_INPUT_STATUSES,
|
|
44
|
+
WISH_MISSION_RUNNER,
|
|
45
|
+
WISH_SWEEP_LIMIT,
|
|
46
|
+
analyzeWishParts,
|
|
47
|
+
auditWish,
|
|
48
|
+
deriveVerifyPlan,
|
|
49
|
+
inferBudgetTier,
|
|
50
|
+
quoteText,
|
|
51
|
+
sharesMeaningfulWords,
|
|
52
|
+
validateEngineOverride,
|
|
53
|
+
verifyOutcomeText,
|
|
54
|
+
wishBodyText,
|
|
55
|
+
} = require('./wish-audit');
|
|
56
|
+
|
|
57
|
+
const METRIC_OPS = { '>=': '--gte', '<=': '--lte', '==': '--eq', '>': '--gt', '<': '--lt' };
|
|
58
|
+
const METRIC_VERIFY_ROOT = '/Users/keshavrao/arena/atrisos-backend';
|
|
59
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
60
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
61
|
+
const IMPLICIT_ANSWER_STALE_MS = HOUR_MS;
|
|
62
|
+
const WISH_LIST_RECENT_DAYS = 7;
|
|
63
|
+
const REVIEW_WORD_SCORES = new Map([
|
|
64
|
+
['great', 5],
|
|
65
|
+
['love it', 5],
|
|
66
|
+
['perfect', 5],
|
|
67
|
+
['good', 4],
|
|
68
|
+
['nice', 4],
|
|
69
|
+
['fine', 3],
|
|
70
|
+
['ok', 3],
|
|
71
|
+
['meh', 3],
|
|
72
|
+
['weak', 2],
|
|
73
|
+
['bad', 1],
|
|
74
|
+
['wrong', 1],
|
|
75
|
+
]);
|
|
76
|
+
const LABEL_SUPERLATIVE_WORDS = new Set([
|
|
77
|
+
'amazing',
|
|
78
|
+
'awesome',
|
|
79
|
+
'best',
|
|
80
|
+
'better',
|
|
81
|
+
'clearest',
|
|
82
|
+
'easiest',
|
|
83
|
+
'fastest',
|
|
84
|
+
'friendliest',
|
|
85
|
+
'good',
|
|
86
|
+
'great',
|
|
87
|
+
'greatest',
|
|
88
|
+
'nicest',
|
|
89
|
+
'perfect',
|
|
90
|
+
'smartest',
|
|
91
|
+
]);
|
|
92
|
+
const LABEL_SUPERLATIVE_PATTERN = Array.from(LABEL_SUPERLATIVE_WORDS).join('|');
|
|
93
|
+
const MAKE_ME_SUPERLATIVE_RE = new RegExp(`\\bmake\\s+me\\s+(?=(?:the\\s+)?(?:${LABEL_SUPERLATIVE_PATTERN})\\b)`, 'ig');
|
|
94
|
+
|
|
95
|
+
// "stripe.active_subs>=10" -> { metric, op, target, flag, expression }.
|
|
96
|
+
function parseMetricExpression(text) {
|
|
97
|
+
const raw = String(text || '').trim();
|
|
98
|
+
const match = raw.match(/^([A-Za-z0-9_][A-Za-z0-9_.-]*)\s*(>=|<=|==|>|<)\s*(-?\d+(?:\.\d+)?)$/);
|
|
99
|
+
if (!match) {
|
|
100
|
+
return { ok: false, message: `Invalid --metric "${raw}": expected <metric><op><number> with op one of >=, >, <=, <, == (example: stripe.active_subs>=10).` };
|
|
101
|
+
}
|
|
102
|
+
const [, metric, op, target] = match;
|
|
103
|
+
return { ok: true, expression: `${metric}${op}${target}`, metric, op, target, flag: METRIC_OPS[op] };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Wire to backend/scripts/metric_verify.py: exits 0 when the metric meets
|
|
107
|
+
// target and prints a JSON line with value + proof_uri.
|
|
108
|
+
function metricVerifierCommand(parsed) {
|
|
109
|
+
return `cd ${METRIC_VERIFY_ROOT} && venv/bin/python backend/scripts/metric_verify.py ${parsed.metric} ${parsed.flag} ${parsed.target}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const MISSION_TERMINAL_STATUSES = new Set(['complete', 'stopped']);
|
|
113
|
+
const WISH_CLOSED_FOR_STEER = new Set(['complete', 'completed', 'captured_no_mission']);
|
|
114
|
+
const DESIGN_BRIEF_INSTRUCTIONS = [
|
|
115
|
+
'read these before writing any ui code',
|
|
116
|
+
'follow the design skill rules and the theme',
|
|
117
|
+
'run the design gate before claiming done',
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
function withRoot(root, fn) {
|
|
121
|
+
const previous = process.cwd();
|
|
122
|
+
if (root && previous !== root) process.chdir(root);
|
|
123
|
+
try {
|
|
124
|
+
return fn();
|
|
125
|
+
} finally {
|
|
126
|
+
if (process.cwd() !== previous) process.chdir(previous);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function captureWishToJournal(text, root = process.cwd()) {
|
|
131
|
+
if (!fs.existsSync(path.join(root, 'atris'))) {
|
|
132
|
+
fs.mkdirSync(path.join(root, 'atris'), { recursive: true });
|
|
133
|
+
}
|
|
134
|
+
ensureLogDirectory();
|
|
135
|
+
const { logFile, dateFormatted } = getLogPath();
|
|
136
|
+
if (!fs.existsSync(logFile)) createLogFile(logFile, dateFormatted);
|
|
137
|
+
const inboxId = addInboxIdea(logFile, text);
|
|
138
|
+
return { logFile, inbox_id: inboxId };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function machineRecord(wish, status, audit, extra = {}) {
|
|
142
|
+
const record = {
|
|
143
|
+
wish_id: wish.id,
|
|
144
|
+
status,
|
|
145
|
+
task_id: extra.task_id || null,
|
|
146
|
+
mission_id: extra.mission_id || null,
|
|
147
|
+
engine: extra.engine || (audit && audit.executor ? audit.executor.id : null),
|
|
148
|
+
budget: audit ? audit.budget : (extra.budget || null),
|
|
149
|
+
questions: audit ? audit.questions : (extra.questions || []),
|
|
150
|
+
};
|
|
151
|
+
const requestedEngine = extra.requested_engine || (audit && audit.requested_engine);
|
|
152
|
+
const engineFallbackReason = extra.engine_fallback_reason || (audit && audit.engine_fallback_reason);
|
|
153
|
+
if (requestedEngine) record.requested_engine = requestedEngine;
|
|
154
|
+
if (engineFallbackReason) record.engine_fallback_reason = engineFallbackReason;
|
|
155
|
+
if (Array.isArray(extra.warnings) && extra.warnings.length) record.warnings = extra.warnings;
|
|
156
|
+
return record;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function engineAuditFields(audit) {
|
|
160
|
+
const fields = {};
|
|
161
|
+
if (audit && audit.requested_engine) fields.requested_engine = audit.requested_engine;
|
|
162
|
+
if (audit && audit.engine_fallback_reason) fields.engine_fallback_reason = audit.engine_fallback_reason;
|
|
163
|
+
return fields;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function wishDelegationEngine(audit, options = {}) {
|
|
167
|
+
if (options.engine) return String(options.engine);
|
|
168
|
+
if (audit && audit.requested_engine && audit.executor && audit.executor.id) return audit.executor.id;
|
|
169
|
+
return WISH_MISSION_RUNNER;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function buildDesignBrief(designContext) {
|
|
173
|
+
if (!designContext || !designContext.hasDesign) return null;
|
|
174
|
+
const paths = Array.isArray(designContext.briefPaths)
|
|
175
|
+
? designContext.briefPaths.map((item) => String(item || '').trim()).filter(Boolean)
|
|
176
|
+
: [];
|
|
177
|
+
if (!paths.length) return null;
|
|
178
|
+
return {
|
|
179
|
+
title: 'design brief',
|
|
180
|
+
paths,
|
|
181
|
+
audit_command: designContext.auditCommand || null,
|
|
182
|
+
instructions: DESIGN_BRIEF_INSTRUCTIONS,
|
|
183
|
+
lines: [
|
|
184
|
+
'design brief',
|
|
185
|
+
...paths.map((briefPath) => `- ${briefPath}`),
|
|
186
|
+
...DESIGN_BRIEF_INSTRUCTIONS.map((instruction) => `- ${instruction}`),
|
|
187
|
+
...(designContext.auditCommand ? [`- design gate: ${designContext.auditCommand}`] : []),
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function appendDesignBriefNote(note, designBrief) {
|
|
193
|
+
if (!designBrief) return;
|
|
194
|
+
note.push('', 'design brief:');
|
|
195
|
+
for (const briefPath of designBrief.paths) note.push(`- ${briefPath}`);
|
|
196
|
+
for (const instruction of designBrief.instructions) note.push(`- ${instruction}`);
|
|
197
|
+
if (designBrief.audit_command) note.push(`- design gate: ${designBrief.audit_command}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function designVerifyPlan(designContext) {
|
|
201
|
+
return {
|
|
202
|
+
command: designContext.auditCommand,
|
|
203
|
+
outcome: 'the design gate passes',
|
|
204
|
+
status: 'design',
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// "#8 haiku loop" -> "#8: haiku loop" so replies read like a person naming the wish.
|
|
209
|
+
function wishTitle(wish) {
|
|
210
|
+
return wishLabel(wish).replace(/^(#\d+)\s+/, '$1: ');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// One question at a time, always ending with how to answer it.
|
|
214
|
+
function printQuestion(wish, question) {
|
|
215
|
+
console.log(`Got it, wish ${wishTitle(wish)}.`);
|
|
216
|
+
console.log(String(question || 'What should be different when this wish comes true?'));
|
|
217
|
+
console.log('Answer with: atris wish answer "your words"');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function printGranted(text, audit, options = {}) {
|
|
221
|
+
const verifyPlan = options.verifyPlan || deriveVerifyPlan(text);
|
|
222
|
+
const subject = String(text || '').trim().replace(/[.!?]+/g, '').replace(/\s+/g, ' ').toLowerCase();
|
|
223
|
+
const change = options.attachedToExistingFlight
|
|
224
|
+
? `i attached ${quoteText(subject)} to work already in flight`
|
|
225
|
+
: `i started work on ${quoteText(subject)}`;
|
|
226
|
+
const proof = verifyPlan.status === 'needs-review'
|
|
227
|
+
? 'after you judge the result i show you'
|
|
228
|
+
: `when ${String(verifyOutcomeText(verifyPlan)).trim().replace(/[.!?]+$/, '').toLowerCase()}`;
|
|
229
|
+
console.log(`${change}, and we will know it worked ${proof}.`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function findActiveWishFlight(wish, objective, owner, root) {
|
|
233
|
+
const wishId = String(wish && wish.id || '').trim();
|
|
234
|
+
const wishFlight = wishId && [
|
|
235
|
+
...listMissions(root),
|
|
236
|
+
...listWorktreeRollupMissions(root),
|
|
237
|
+
].find((mission) => (
|
|
238
|
+
TWIN_ACTIVE_STATUSES.has(mission.status)
|
|
239
|
+
&& (mission.wish_id === wishId || mission.metadata?.wish_id === wishId)
|
|
240
|
+
));
|
|
241
|
+
return wishFlight || findActiveTwinMission(objective, owner, root);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function latestMissionStatus(root, missionId) {
|
|
245
|
+
if (!missionId) return '';
|
|
246
|
+
try {
|
|
247
|
+
const mission = listMissions(root).find((row) => row.id === missionId);
|
|
248
|
+
return mission ? String(mission.status || '') : '';
|
|
249
|
+
} catch {
|
|
250
|
+
return '';
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Everything the operator sees maps to five plain words:
|
|
255
|
+
// working / done / stuck / waiting on you / new. Storage values stay unchanged.
|
|
256
|
+
function operatorStatus(wish, root = process.cwd()) {
|
|
257
|
+
const status = String(wish.status || '');
|
|
258
|
+
if (WAITING_INPUT_STATUSES.has(status)) return 'waiting on you';
|
|
259
|
+
if (status === 'builder') return 'waiting on you';
|
|
260
|
+
if (status === 'complete' || status === 'completed') return 'done';
|
|
261
|
+
if (status === 'delegated' || status === 'decomposed') {
|
|
262
|
+
const missionStatus = latestMissionStatus(root, wish.mission_id);
|
|
263
|
+
if (missionStatus === 'complete') return 'done';
|
|
264
|
+
if (missionStatus === 'stopped' || missionStatus === 'blocked') return 'stuck';
|
|
265
|
+
if (missionStatus === 'paused' || missionStatus === 'ready') return 'waiting on you';
|
|
266
|
+
if (missionStatus) return 'working';
|
|
267
|
+
if (Array.isArray(wish.out_of_scope_parts) && wish.out_of_scope_parts.length) return 'stuck';
|
|
268
|
+
return 'working';
|
|
269
|
+
}
|
|
270
|
+
return 'new';
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function reviewedMarker(wish) {
|
|
274
|
+
return wish && (wish.reviewed || (Array.isArray(wish.reviews) && wish.reviews.length)) ? ' [reviewed]' : '';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function wishHasReview(wish) {
|
|
278
|
+
return !!(wish && (wish.reviewed || (Array.isArray(wish.reviews) && wish.reviews.length)));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function wishLabel(wish, options = {}) {
|
|
282
|
+
const text = wishLabelText(wish);
|
|
283
|
+
const wordLimit = options.wordLimit || wishLabelWordLimit(text);
|
|
284
|
+
return shortRecordLabel(wish, text, { wordLimit });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function wishLabelText(wish) {
|
|
288
|
+
return String(wish && (wish.text || wish.task_text || wish.id) || '')
|
|
289
|
+
.replace(MAKE_ME_SUPERLATIVE_RE, 'make ')
|
|
290
|
+
.replace(/\bever\b/ig, ' ')
|
|
291
|
+
.replace(/\s+/g, ' ')
|
|
292
|
+
.trim();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function wishLabelWordLimit(text) {
|
|
296
|
+
const words = String(text || '')
|
|
297
|
+
.toLowerCase()
|
|
298
|
+
.match(/[a-z0-9][a-z0-9']*/g) || [];
|
|
299
|
+
const index = words.findIndex((word) => LABEL_SUPERLATIVE_WORDS.has(word));
|
|
300
|
+
if (index < 0) return 3;
|
|
301
|
+
const after = words.slice(index + 1).filter((word) => !['a', 'an', 'the'].includes(word));
|
|
302
|
+
return after.length >= 2 ? 4 : 3;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function wishRef(wish) {
|
|
306
|
+
return shortRecordRef(wish);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function wishMatchesRef(wish, ref) {
|
|
310
|
+
return recordMatchesRef(wish, ref);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function answerWishMatchesRef(wish, ref) {
|
|
314
|
+
if (wishMatchesRef(wish, ref)) return true;
|
|
315
|
+
return !!(wish && wish._answer_parent && wishMatchesRef(wish._answer_parent, ref));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function openWishes(root = process.cwd()) {
|
|
319
|
+
return readWishes(root).filter((wish) => ['needs_input', 'waiting_input', 'delegated', 'decomposed', 'complete', 'builder', 'captured', 'captured_no_mission'].includes(String(wish.status || '')));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function parseWishTime(value) {
|
|
323
|
+
const parsed = Date.parse(value || '');
|
|
324
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function latestWishMoveTime(wish) {
|
|
328
|
+
const times = [
|
|
329
|
+
wish && wish.updated_at,
|
|
330
|
+
wish && wish.completed_at,
|
|
331
|
+
wish && wish.verified_at,
|
|
332
|
+
wish && wish.dispatched_at,
|
|
333
|
+
wish && wish.ts,
|
|
334
|
+
wish && wish.first_ts,
|
|
335
|
+
wish && wish.latest_review && wish.latest_review.ts,
|
|
336
|
+
wish && wish.latest_steer && wish.latest_steer.ts,
|
|
337
|
+
].map(parseWishTime).filter((time) => time !== null);
|
|
338
|
+
return times.length ? Math.max(...times) : null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function shippedWish(wish, root = process.cwd()) {
|
|
342
|
+
if (!wish || !wish.id) return false;
|
|
343
|
+
const status = String(wish.status || '').trim();
|
|
344
|
+
const verifyStatus = String(wish.verify_status || '').trim();
|
|
345
|
+
if (status === 'complete' || status === 'completed') return true;
|
|
346
|
+
if (['verified', 'passed', 'success'].includes(verifyStatus)) return true;
|
|
347
|
+
return status === 'delegated' && latestMissionStatus(root, wish.mission_id) === 'complete';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function shippedWishTime(wish) {
|
|
351
|
+
const status = String(wish && wish.status || '').trim();
|
|
352
|
+
const verifyStatus = String(wish && wish.verify_status || '').trim();
|
|
353
|
+
const fields = status === 'complete' || status === 'completed'
|
|
354
|
+
? ['completed_at', 'updated_at', 'ts', 'verified_at', 'dispatched_at', 'first_ts']
|
|
355
|
+
: (['verified', 'passed', 'success'].includes(verifyStatus)
|
|
356
|
+
? ['verified_at', 'updated_at', 'ts', 'completed_at', 'dispatched_at', 'first_ts']
|
|
357
|
+
: ['completed_at', 'verified_at', 'updated_at', 'ts', 'dispatched_at', 'first_ts']);
|
|
358
|
+
return fields.map((field) => parseWishTime(wish && wish[field])).find((time) => time !== null) ?? null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function staleUnreviewedShippedWish(wish, root = process.cwd(), now = Date.now()) {
|
|
362
|
+
if (!shippedWish(wish, root) || wishHasReview(wish)) return false;
|
|
363
|
+
const shippedAt = shippedWishTime(wish);
|
|
364
|
+
return shippedAt !== null && now - shippedAt >= DAY_MS;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function listStatus(wish, root = process.cwd(), now = Date.now()) {
|
|
368
|
+
if (staleUnreviewedShippedWish(wish, root, now)) return 'done, needs your review';
|
|
369
|
+
return operatorStatus(wish, root);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function listedWishes(root = process.cwd(), options = {}) {
|
|
373
|
+
const all = options.all === true;
|
|
374
|
+
const wishes = openWishes(root);
|
|
375
|
+
if (all) return { visible: wishes, resting: [] };
|
|
376
|
+
const now = Number.isFinite(options.now) ? options.now : Date.now();
|
|
377
|
+
const cutoff = now - (WISH_LIST_RECENT_DAYS * DAY_MS);
|
|
378
|
+
const visible = [];
|
|
379
|
+
const resting = [];
|
|
380
|
+
wishes.forEach((wish) => {
|
|
381
|
+
const status = operatorStatus(wish, root);
|
|
382
|
+
const movedAt = latestWishMoveTime(wish);
|
|
383
|
+
if (status === 'working' || status === 'waiting on you' || (movedAt !== null && movedAt >= cutoff)) {
|
|
384
|
+
visible.push(wish);
|
|
385
|
+
} else {
|
|
386
|
+
resting.push(wish);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
return { visible, resting };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function printList(root = process.cwd(), options = {}) {
|
|
393
|
+
const { visible, resting } = listedWishes(root, options);
|
|
394
|
+
const total = visible.length + resting.length;
|
|
395
|
+
const now = Number.isFinite(options.now) ? options.now : Date.now();
|
|
396
|
+
if (!total) {
|
|
397
|
+
console.log('No open wishes.');
|
|
398
|
+
return 0;
|
|
399
|
+
}
|
|
400
|
+
visible.forEach((wish, index) => {
|
|
401
|
+
// The list is a human catch-up surface: give the label enough words to
|
|
402
|
+
// carry the wish's meaning (the 3-word handle stays for say/close refs).
|
|
403
|
+
console.log(`${index + 1}. ${wishLabel(wish, { wordLimit: 12 })} - ${listStatus(wish, root, now)}${reviewedMarker(wish)}`);
|
|
404
|
+
});
|
|
405
|
+
if (resting.length) {
|
|
406
|
+
console.log(`${resting.length} older ${resting.length === 1 ? 'wish is' : 'wishes are'} resting. See ${resting.length === 1 ? 'it' : 'them'} with atris wish list --all`);
|
|
407
|
+
}
|
|
408
|
+
return 0;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function actorName() {
|
|
412
|
+
return process.env.ATRIS_AGENT_ID || process.env.USER || 'operator';
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function shortText(value, width = 52) {
|
|
416
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
417
|
+
if (text.length <= width) return text;
|
|
418
|
+
return text.slice(0, Math.max(0, width - 3)).trimEnd() + '...';
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function renderAlignedTable(headers, rows) {
|
|
422
|
+
const widths = headers.map((header, index) => {
|
|
423
|
+
const rowWidth = rows.reduce((max, row) => Math.max(max, String(row[index] || '').length), 0);
|
|
424
|
+
return Math.max(String(header).length, rowWidth);
|
|
425
|
+
});
|
|
426
|
+
const renderRow = (row) => row.map((cell, index) => String(cell || '').padEnd(widths[index])).join(' ').trimEnd();
|
|
427
|
+
const separator = widths.map((width) => '-'.repeat(width)).join(' ');
|
|
428
|
+
return [renderRow(headers), separator, ...rows.map(renderRow)].join('\n');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function latestReview(wish) {
|
|
432
|
+
if (wish && wish.latest_review) return wish.latest_review;
|
|
433
|
+
const reviews = Array.isArray(wish && wish.reviews) ? wish.reviews : [];
|
|
434
|
+
return reviews.length ? reviews[reviews.length - 1] : null;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function reviewColumn(wish) {
|
|
438
|
+
const review = latestReview(wish);
|
|
439
|
+
if (!review) return '-';
|
|
440
|
+
const score = review.review_score === undefined || review.review_score === null ? 'n/a' : String(review.review_score);
|
|
441
|
+
return `reviewed/${score}`;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function printBoard(root = process.cwd()) {
|
|
445
|
+
const wishes = readWishes(root);
|
|
446
|
+
if (!wishes.length) {
|
|
447
|
+
console.log('No wishes.');
|
|
448
|
+
return 0;
|
|
449
|
+
}
|
|
450
|
+
const rows = wishes.map((wish) => [
|
|
451
|
+
wishLabel(wish),
|
|
452
|
+
shortText(wish.text || wish.task_text || ''),
|
|
453
|
+
operatorStatus(wish, root),
|
|
454
|
+
wish.engine || wish.requested_engine || '-',
|
|
455
|
+
wish.verify_status || '-',
|
|
456
|
+
reviewColumn(wish),
|
|
457
|
+
]);
|
|
458
|
+
console.log(renderAlignedTable(['wish', 'text', 'status', 'engine', 'verify_status', 'review'], rows));
|
|
459
|
+
return 0;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function reviewNudgeWishes(root = process.cwd()) {
|
|
463
|
+
return readWishes(root).filter((wish) => {
|
|
464
|
+
if (!wish || !wish.id) return false;
|
|
465
|
+
if (wishHasReview(wish)) return false;
|
|
466
|
+
if (!shippedWish(wish, root)) return false;
|
|
467
|
+
return !staleUnreviewedShippedWish(wish, root);
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function printReviewNudges(root = process.cwd()) {
|
|
472
|
+
const wishes = reviewNudgeWishes(root);
|
|
473
|
+
if (!wishes.length) return 0;
|
|
474
|
+
console.log('Wishes ready for review:');
|
|
475
|
+
wishes.forEach((wish) => {
|
|
476
|
+
console.log(`- ${wishLabel(wish)}: atris wish review ${wishRef(wish)} "<one sentence>"`);
|
|
477
|
+
});
|
|
478
|
+
return 0;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function scorecardText(value) {
|
|
482
|
+
if (value === undefined || value === null) return '';
|
|
483
|
+
if (typeof value === 'string') return value;
|
|
484
|
+
try {
|
|
485
|
+
return JSON.stringify(value);
|
|
486
|
+
} catch {
|
|
487
|
+
return String(value);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function readWishReviewRewardRows(root = process.cwd()) {
|
|
492
|
+
const file = path.resolve(root, '..', 'atrisos-backend', '.atris', 'state', 'scorecards.jsonl');
|
|
493
|
+
return readJsonLines(file).filter((row) => {
|
|
494
|
+
const haystack = `${scorecardText(row.source)} ${scorecardText(row.feedback)}`;
|
|
495
|
+
return /wish_review/i.test(haystack);
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function averageReviewScore(reviews) {
|
|
500
|
+
const scores = reviews
|
|
501
|
+
.filter((review) => review.review_score !== undefined && review.review_score !== null && String(review.review_score).trim() !== '')
|
|
502
|
+
.map((review) => Number(review.review_score))
|
|
503
|
+
.filter((score) => Number.isFinite(score));
|
|
504
|
+
if (!scores.length) return 'n/a';
|
|
505
|
+
const value = scores.reduce((sum, score) => sum + score, 0) / scores.length;
|
|
506
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(2);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function reviewLine(review) {
|
|
510
|
+
const score = review.review_score === undefined || review.review_score === null ? 'n/a' : String(review.review_score);
|
|
511
|
+
const day = String(review.ts || '').slice(0, 10) || 'unknown-date';
|
|
512
|
+
return `${day} score=${score} ${review.wish_id || 'unknown'} ${shortText(review.review_text || '', 80)}`.trim();
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function printRewards(root = process.cwd()) {
|
|
516
|
+
const reviews = readWishEvents(root).filter((event) => event && event.kind === 'review');
|
|
517
|
+
const rewardRows = readWishReviewRewardRows(root);
|
|
518
|
+
console.log(`reviews count: ${reviews.length}`);
|
|
519
|
+
console.log(`avg score: ${averageReviewScore(reviews)}`);
|
|
520
|
+
console.log('last 5 review lines:');
|
|
521
|
+
const lastFive = reviews.slice(-5);
|
|
522
|
+
if (!lastFive.length) console.log('- none');
|
|
523
|
+
else lastFive.forEach((review) => console.log(`- ${reviewLine(review)}`));
|
|
524
|
+
console.log(`reward rows found: ${rewardRows.length}`);
|
|
525
|
+
return 0;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function reviewableWish(wish) {
|
|
529
|
+
const status = String(wish && wish.status || '').trim();
|
|
530
|
+
// decomposed = split into delegated part-missions; work is in flight or
|
|
531
|
+
// landed, so it is as reviewable as a directly delegated wish.
|
|
532
|
+
return status === 'delegated' || status === 'decomposed' || status === 'complete' || status === 'completed';
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function wishReviewTime(wish, fallback) {
|
|
536
|
+
const parsed = Date.parse(wish.completed_at || wish.dispatched_at || wish.ts || wish.first_ts || '');
|
|
537
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function latestReviewableWish(wishes) {
|
|
541
|
+
let latest = null;
|
|
542
|
+
let latestTime = -Infinity;
|
|
543
|
+
wishes.forEach((wish, index) => {
|
|
544
|
+
if (!reviewableWish(wish)) return;
|
|
545
|
+
const time = wishReviewTime(wish, index);
|
|
546
|
+
if (time >= latestTime) {
|
|
547
|
+
latest = wish;
|
|
548
|
+
latestTime = time;
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
return latest;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function looksLikeWishId(value) {
|
|
555
|
+
return /^wish[-_]/i.test(String(value || '').trim());
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function reviewWordKey(value) {
|
|
559
|
+
return String(value || '')
|
|
560
|
+
.toLowerCase()
|
|
561
|
+
.replace(/[.!?]+$/g, '')
|
|
562
|
+
.replace(/\s+/g, ' ')
|
|
563
|
+
.trim();
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function reviewWordScore(value) {
|
|
567
|
+
const key = reviewWordKey(value);
|
|
568
|
+
return REVIEW_WORD_SCORES.has(key) ? REVIEW_WORD_SCORES.get(key) : null;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function resolveReviewTarget(root, ref) {
|
|
572
|
+
const wishes = readWishes(root);
|
|
573
|
+
if (!wishes.length) {
|
|
574
|
+
return { ok: false, message: 'No wishes to review yet.' };
|
|
575
|
+
}
|
|
576
|
+
if (!ref || ref === 'latest') {
|
|
577
|
+
const latest = latestReviewableWish(wishes);
|
|
578
|
+
if (!latest) return { ok: false, message: 'No delegated or completed wishes to review.' };
|
|
579
|
+
return { ok: true, wish: latest };
|
|
580
|
+
}
|
|
581
|
+
const wish = wishes.find((row) => wishMatchesRef(row, ref));
|
|
582
|
+
if (!wish) return { ok: false, message: `No wish found with id ${ref}.` };
|
|
583
|
+
return { ok: true, wish };
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function wishSteerTime(wish, fallback) {
|
|
587
|
+
const parsed = Date.parse(
|
|
588
|
+
wish.updated_at
|
|
589
|
+
|| (wish.latest_steer && wish.latest_steer.ts)
|
|
590
|
+
|| wish.dispatched_at
|
|
591
|
+
|| wish.ts
|
|
592
|
+
|| wish.first_ts
|
|
593
|
+
|| '',
|
|
594
|
+
);
|
|
595
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function steerableWish(wish, root = process.cwd()) {
|
|
599
|
+
if (!wish || !wish.id) return false;
|
|
600
|
+
const status = String(wish.status || '').trim();
|
|
601
|
+
if (WISH_CLOSED_FOR_STEER.has(status)) return false;
|
|
602
|
+
const missionStatus = latestMissionStatus(root, wish.mission_id);
|
|
603
|
+
return !MISSION_TERMINAL_STATUSES.has(missionStatus);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function latestSteerableWish(wishes, root = process.cwd()) {
|
|
607
|
+
let latest = null;
|
|
608
|
+
let latestTime = -Infinity;
|
|
609
|
+
wishes.forEach((wish, index) => {
|
|
610
|
+
if (!steerableWish(wish, root)) return;
|
|
611
|
+
const time = wishSteerTime(wish, index);
|
|
612
|
+
if (time >= latestTime) {
|
|
613
|
+
latest = wish;
|
|
614
|
+
latestTime = time;
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
return latest;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function resolveSteerTarget(root, ref) {
|
|
621
|
+
const wishes = readWishes(root);
|
|
622
|
+
if (!wishes.length) {
|
|
623
|
+
return { ok: false, message: 'No wishes to steer yet.' };
|
|
624
|
+
}
|
|
625
|
+
if (!ref || ref === 'latest') {
|
|
626
|
+
const latest = latestSteerableWish(wishes, root);
|
|
627
|
+
if (!latest) return { ok: false, message: 'No open wishes to steer.' };
|
|
628
|
+
return { ok: true, wish: latest };
|
|
629
|
+
}
|
|
630
|
+
const wish = wishes.find((row) => wishMatchesRef(row, ref));
|
|
631
|
+
if (!wish) return { ok: false, message: `No wish found with id ${ref}.` };
|
|
632
|
+
return { ok: true, wish };
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function liveLinkedMission(root, missionId) {
|
|
636
|
+
if (!missionId) return null;
|
|
637
|
+
try {
|
|
638
|
+
const mission = listMissions(root).find((row) => row.id === missionId);
|
|
639
|
+
if (!mission || MISSION_TERMINAL_STATUSES.has(String(mission.status || ''))) return null;
|
|
640
|
+
return mission;
|
|
641
|
+
} catch {
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function deliverSteerToMission(wish, note, root, from) {
|
|
647
|
+
const mission = liveLinkedMission(root, wish.mission_id);
|
|
648
|
+
if (!mission) return null;
|
|
649
|
+
return withRoot(root, () => pingMission([mission.id, note, '--from', from], { silent: true }));
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function sayWish(positionals, root = process.cwd()) {
|
|
653
|
+
const parts = positionals.slice(1).map(String).filter((value) => value.trim());
|
|
654
|
+
if (!parts.length) {
|
|
655
|
+
console.error('wish say needs a note.');
|
|
656
|
+
return 2;
|
|
657
|
+
}
|
|
658
|
+
const wishes = readWishes(root);
|
|
659
|
+
const isKnownRef = (value) => value === 'latest' || wishes.some((wish) => wishMatchesRef(wish, value)) || looksLikeWishId(value);
|
|
660
|
+
let targetRef = 'latest';
|
|
661
|
+
let noteParts = parts;
|
|
662
|
+
const first = parts[0];
|
|
663
|
+
const last = parts[parts.length - 1];
|
|
664
|
+
if (parts.length > 1 && isKnownRef(last)) {
|
|
665
|
+
targetRef = last;
|
|
666
|
+
noteParts = parts.slice(0, -1);
|
|
667
|
+
} else if (parts.length > 1 && isKnownRef(first)) {
|
|
668
|
+
targetRef = first;
|
|
669
|
+
noteParts = parts.slice(1);
|
|
670
|
+
}
|
|
671
|
+
const note = noteParts.join(' ').trim();
|
|
672
|
+
if (!note) {
|
|
673
|
+
console.error('wish say needs a note.');
|
|
674
|
+
return 2;
|
|
675
|
+
}
|
|
676
|
+
const target = resolveSteerTarget(root, targetRef);
|
|
677
|
+
if (!target.ok) {
|
|
678
|
+
console.error(target.message);
|
|
679
|
+
return 2;
|
|
680
|
+
}
|
|
681
|
+
const from = actorName();
|
|
682
|
+
appendWishRecord(root, {
|
|
683
|
+
kind: 'steer',
|
|
684
|
+
wish_id: target.wish.id,
|
|
685
|
+
ts: stampIso(),
|
|
686
|
+
note,
|
|
687
|
+
steered_by: from,
|
|
688
|
+
mission_id: target.wish.mission_id || null,
|
|
689
|
+
});
|
|
690
|
+
const mission = deliverSteerToMission(target.wish, note, root, from);
|
|
691
|
+
const suffix = mission ? ` and sent to ${mission.id}` : '';
|
|
692
|
+
console.log(`Steering captured for ${quoteText(target.wish.text || target.wish.id)}${suffix}.`);
|
|
693
|
+
return 0;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function reviewWish(positionals, root = process.cwd(), options = {}) {
|
|
697
|
+
const score = { ok: true, value: options.reviewScore === undefined ? null : options.reviewScore };
|
|
698
|
+
const parts = positionals.slice(1).map(String).filter((value) => value.trim());
|
|
699
|
+
if (!parts.length) {
|
|
700
|
+
console.error('wish review needs one sentence of feedback.');
|
|
701
|
+
return 2;
|
|
702
|
+
}
|
|
703
|
+
let targetRef = 'latest';
|
|
704
|
+
let reviewParts = parts;
|
|
705
|
+
const first = parts[0];
|
|
706
|
+
const wishes = readWishes(root);
|
|
707
|
+
if (first === 'latest' || /^\#?\d+$/.test(first) || looksLikeWishId(first) || wishes.some((wish) => wishMatchesRef(wish, first))) {
|
|
708
|
+
targetRef = first;
|
|
709
|
+
reviewParts = parts.slice(1);
|
|
710
|
+
}
|
|
711
|
+
const reviewText = reviewParts.join(' ').trim();
|
|
712
|
+
if (!reviewText) {
|
|
713
|
+
console.error('wish review needs one sentence of feedback.');
|
|
714
|
+
return 2;
|
|
715
|
+
}
|
|
716
|
+
const target = resolveReviewTarget(root, targetRef);
|
|
717
|
+
if (!target.ok) {
|
|
718
|
+
console.error(target.message);
|
|
719
|
+
return 2;
|
|
720
|
+
}
|
|
721
|
+
appendWishRecord(root, {
|
|
722
|
+
kind: 'review',
|
|
723
|
+
wish_id: target.wish.id,
|
|
724
|
+
ts: stampIso(),
|
|
725
|
+
review_text: reviewText,
|
|
726
|
+
review_score: score.value === null ? reviewWordScore(reviewText) : score.value,
|
|
727
|
+
reviewed_by: actorName(),
|
|
728
|
+
});
|
|
729
|
+
console.log(`Review saved for ${quoteText(target.wish.text || target.wish.id)}. It will shape the next run.`);
|
|
730
|
+
return 0;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function startWishDelegation(wish, audit, root, options = {}) {
|
|
734
|
+
try {
|
|
735
|
+
improveWishes(root, { quiet: true });
|
|
736
|
+
} catch {
|
|
737
|
+
// best-effort: lessons ingest must never block a wish run
|
|
738
|
+
}
|
|
739
|
+
const taskText = String(options.taskText || wish.text);
|
|
740
|
+
const recordText = String(options.recordText || wish.text);
|
|
741
|
+
const metricOption = options.metric && options.metric.ok
|
|
742
|
+
? options.metric
|
|
743
|
+
: (wish.metric ? parseMetricExpression(wish.metric) : null);
|
|
744
|
+
const metricPlan = metricOption && metricOption.ok ? metricOption : null;
|
|
745
|
+
const frontendWish = isFrontendWish(taskText);
|
|
746
|
+
const designContext = frontendWish ? detectDesignContext(root) : { hasDesign: false, briefPaths: [], auditCommand: null };
|
|
747
|
+
const designBrief = frontendWish && designContext.hasDesign ? buildDesignBrief(designContext) : null;
|
|
748
|
+
const suppliedVerifyPlan = options.verifyPlan || null;
|
|
749
|
+
const suppliedVerifyHasCommand = !!(suppliedVerifyPlan && suppliedVerifyPlan.command);
|
|
750
|
+
const userVerifyProvided = options.userVerifyProvided === true || options.verifyPlanExplicit === true;
|
|
751
|
+
let verifyPlan = metricPlan
|
|
752
|
+
? { command: metricVerifierCommand(metricPlan), outcome: `metric ${metricPlan.expression} is met`, status: 'metric' }
|
|
753
|
+
: (suppliedVerifyPlan || deriveVerifyPlan(taskText));
|
|
754
|
+
if (!metricPlan && frontendWish && designContext.auditCommand && !userVerifyProvided && !suppliedVerifyHasCommand && !(verifyPlan && verifyPlan.command)) {
|
|
755
|
+
verifyPlan = designVerifyPlan(designContext);
|
|
756
|
+
}
|
|
757
|
+
const missionVerifier = verifyPlan.command || resolveDefaultVerifier(root);
|
|
758
|
+
const verifyWarning = verifierBudgetWarning(missionVerifier);
|
|
759
|
+
const engine = wishDelegationEngine(audit, options);
|
|
760
|
+
const now = stampIso();
|
|
761
|
+
const ownerResolution = resolveFunctionalOwner({
|
|
762
|
+
requestedOwner: audit.executor.id,
|
|
763
|
+
title: taskText,
|
|
764
|
+
tag: 'wish',
|
|
765
|
+
goal: taskText,
|
|
766
|
+
root,
|
|
767
|
+
fallbackOwners: ['mission-lead', 'task-planner', 'architect', 'validator'],
|
|
768
|
+
});
|
|
769
|
+
const baseRoom = buildMissionRoom(taskText, {
|
|
770
|
+
owner: ownerResolution.owner,
|
|
771
|
+
root,
|
|
772
|
+
ownerResolution,
|
|
773
|
+
trustedRun: true,
|
|
774
|
+
verifier: verifyPlan.command || '',
|
|
775
|
+
});
|
|
776
|
+
const routeOwner = baseRoom.member_route?.suggested_member
|
|
777
|
+
|| baseRoom.owner
|
|
778
|
+
|| ownerResolution.owner
|
|
779
|
+
|| 'mission-lead';
|
|
780
|
+
const existingFlight = findActiveWishFlight(wish, taskText, routeOwner, root);
|
|
781
|
+
if (existingFlight) {
|
|
782
|
+
const flightVerifyWarning = verifierBudgetWarning(existingFlight.verifier || missionVerifier);
|
|
783
|
+
const taskId = existingFlight.current_task_id || existingFlight.task_ids?.[0] || null;
|
|
784
|
+
const record = {
|
|
785
|
+
id: wish.id,
|
|
786
|
+
ts: now,
|
|
787
|
+
text: recordText,
|
|
788
|
+
status: 'delegated',
|
|
789
|
+
parent_id: wish.parent_id || undefined,
|
|
790
|
+
dispatched_at: now,
|
|
791
|
+
task_id: taskId,
|
|
792
|
+
mission_id: existingFlight.id,
|
|
793
|
+
engine: existingFlight.runner || engine,
|
|
794
|
+
validator: audit.validator.id,
|
|
795
|
+
budget: audit.budget,
|
|
796
|
+
...(metricPlan ? { metric: metricPlan.expression } : {}),
|
|
797
|
+
verify: verifyPlan.command,
|
|
798
|
+
verify_status: verifyPlan.status,
|
|
799
|
+
verify_outcome: verifyPlan.outcome,
|
|
800
|
+
...(flightVerifyWarning ? { verify_warning: flightVerifyWarning } : {}),
|
|
801
|
+
task_text: taskText,
|
|
802
|
+
mission_room_receipt_path: existingFlight.mission_room_receipt_path || null,
|
|
803
|
+
mission_room_name: existingFlight.mission_room_name || null,
|
|
804
|
+
mission_owner: existingFlight.owner || routeOwner,
|
|
805
|
+
attached_to_existing_flight: true,
|
|
806
|
+
...(designBrief ? {
|
|
807
|
+
design_brief_paths: designBrief.paths,
|
|
808
|
+
design_audit_command: designBrief.audit_command,
|
|
809
|
+
} : {}),
|
|
810
|
+
...engineAuditFields(audit),
|
|
811
|
+
};
|
|
812
|
+
return {
|
|
813
|
+
record,
|
|
814
|
+
taskPayload: { task_id: taskId, action: 'flight_attached' },
|
|
815
|
+
mission: existingFlight,
|
|
816
|
+
verifyPlan,
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
const room = {
|
|
820
|
+
...baseRoom,
|
|
821
|
+
wish_id: wish.id,
|
|
822
|
+
wish: {
|
|
823
|
+
id: wish.id,
|
|
824
|
+
text: recordText,
|
|
825
|
+
task_text: taskText,
|
|
826
|
+
parent_id: wish.parent_id || null,
|
|
827
|
+
},
|
|
828
|
+
source: {
|
|
829
|
+
...(baseRoom.source || {}),
|
|
830
|
+
wish_id: wish.id,
|
|
831
|
+
wish_text: recordText,
|
|
832
|
+
wish_parent_id: wish.parent_id || null,
|
|
833
|
+
},
|
|
834
|
+
...(designBrief ? { design_brief: designBrief } : {}),
|
|
835
|
+
};
|
|
836
|
+
const writtenRoom = writeMissionRoomReceipt(room, { root });
|
|
837
|
+
const note = [
|
|
838
|
+
`Wish: ${taskText}`,
|
|
839
|
+
`Wish label: ${wishLabel(wish)}`,
|
|
840
|
+
`Mission Room: ${writtenRoom.relativePath}`,
|
|
841
|
+
verifyPlan.command
|
|
842
|
+
? `Verify: ${verifyPlan.command} (${verifyOutcomeText(verifyPlan)})`
|
|
843
|
+
: `Verify: needs human review (${verifyPlan.outcome})`,
|
|
844
|
+
`Budget: ${audit.budget}`,
|
|
845
|
+
];
|
|
846
|
+
if (verifyWarning) note.push(`Verifier warning: ${verifyWarning.message.replace(/^warning:\s*/i, '')}`);
|
|
847
|
+
appendDesignBriefNote(note, designBrief);
|
|
848
|
+
let lessonsBlock = '';
|
|
849
|
+
try {
|
|
850
|
+
lessonsBlock = wishLessonsBrief(root);
|
|
851
|
+
} catch {
|
|
852
|
+
lessonsBlock = '';
|
|
853
|
+
}
|
|
854
|
+
if (lessonsBlock) note.push('', lessonsBlock);
|
|
855
|
+
const noteText = note.join('\n');
|
|
856
|
+
let taskPayload = null;
|
|
857
|
+
const missionPayload = withRoot(root, () => {
|
|
858
|
+
const missionArgs = [
|
|
859
|
+
taskText,
|
|
860
|
+
'--owner',
|
|
861
|
+
routeOwner,
|
|
862
|
+
'--runner',
|
|
863
|
+
engine,
|
|
864
|
+
'--budget',
|
|
865
|
+
audit.budget,
|
|
866
|
+
'--json',
|
|
867
|
+
];
|
|
868
|
+
missionArgs.push('--verify', missionVerifier);
|
|
869
|
+
return startMission(missionArgs, {
|
|
870
|
+
silent: true,
|
|
871
|
+
beforeMissionSave: (mission) => {
|
|
872
|
+
taskPayload = delegateTask([
|
|
873
|
+
taskText,
|
|
874
|
+
'--to',
|
|
875
|
+
routeOwner,
|
|
876
|
+
'--executed-by',
|
|
877
|
+
engine,
|
|
878
|
+
'--tag',
|
|
879
|
+
'wish',
|
|
880
|
+
'--goal-objective',
|
|
881
|
+
taskText,
|
|
882
|
+
'--note',
|
|
883
|
+
noteText,
|
|
884
|
+
], { warnOperatorTitle: false });
|
|
885
|
+
return {
|
|
886
|
+
...mission,
|
|
887
|
+
task_ids: Array.from(new Set([...(mission.task_ids || []), taskPayload.task_id])),
|
|
888
|
+
};
|
|
889
|
+
},
|
|
890
|
+
missionPatch: {
|
|
891
|
+
wish_id: wish.id,
|
|
892
|
+
wish_text: recordText,
|
|
893
|
+
mission_room_receipt_path: writtenRoom.relativePath,
|
|
894
|
+
mission_room_name: writtenRoom.room?.name || null,
|
|
895
|
+
source: 'wish',
|
|
896
|
+
...(verifyWarning ? { verifier_budget_warning: verifyWarning } : {}),
|
|
897
|
+
...(designBrief ? {
|
|
898
|
+
design_brief_paths: designBrief.paths,
|
|
899
|
+
design_audit_command: designBrief.audit_command,
|
|
900
|
+
} : {}),
|
|
901
|
+
...(metricPlan ? { stop_condition: 'verifier green (metric target hit)' } : {}),
|
|
902
|
+
metadata: {
|
|
903
|
+
wish_id: wish.id,
|
|
904
|
+
wish_text: recordText,
|
|
905
|
+
...(metricPlan ? { metric: metricPlan.expression } : {}),
|
|
906
|
+
mission_room_receipt_path: writtenRoom.relativePath,
|
|
907
|
+
mission_room_name: writtenRoom.room?.name || null,
|
|
908
|
+
mission_room_owner: routeOwner,
|
|
909
|
+
...(verifyWarning ? { verifier_budget_warning: verifyWarning } : {}),
|
|
910
|
+
...(designBrief ? {
|
|
911
|
+
design_brief_paths: designBrief.paths,
|
|
912
|
+
design_audit_command: designBrief.audit_command,
|
|
913
|
+
} : {}),
|
|
914
|
+
},
|
|
915
|
+
},
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
const mission = missionPayload && missionPayload.mission ? missionPayload.mission : null;
|
|
919
|
+
const reused = missionPayload && missionPayload.action === 'mission_reused';
|
|
920
|
+
// A delegated wish must have a live worker, not just a filed mission. Start
|
|
921
|
+
// the detached driver for any runner that can work headlessly; runners that
|
|
922
|
+
// need a live session keep an honest not-started marker instead.
|
|
923
|
+
let driver = null;
|
|
924
|
+
if (mission && !missionRunnerNeedsLiveSession(mission.runner) && process.env.ATRIS_WISH_NO_DRIVER !== '1') {
|
|
925
|
+
driver = spawnMissionDriver(mission, root);
|
|
926
|
+
}
|
|
927
|
+
const workerStarted = Boolean(driver && driver.ok && (driver.pid || driver.already_running));
|
|
928
|
+
const taskId = reused
|
|
929
|
+
? (mission?.current_task_id || mission?.task_ids?.[0] || null)
|
|
930
|
+
: taskPayload?.task_id || null;
|
|
931
|
+
const record = {
|
|
932
|
+
id: wish.id,
|
|
933
|
+
ts: now,
|
|
934
|
+
text: recordText,
|
|
935
|
+
status: 'delegated',
|
|
936
|
+
parent_id: wish.parent_id || undefined,
|
|
937
|
+
dispatched_at: now,
|
|
938
|
+
task_id: taskId,
|
|
939
|
+
mission_id: mission ? mission.id : null,
|
|
940
|
+
worker_started: workerStarted,
|
|
941
|
+
...(driver && driver.pid ? { driver_pid: driver.pid } : {}),
|
|
942
|
+
...(mission && !workerStarted ? {
|
|
943
|
+
worker_note: missionRunnerNeedsLiveSession(mission.runner)
|
|
944
|
+
? `waiting for a live ${mission.runner || 'session'} to pick this up`
|
|
945
|
+
: `worker did not start; run: atris mission run ${mission.id}`,
|
|
946
|
+
} : {}),
|
|
947
|
+
engine: reused ? (mission?.runner || engine) : engine,
|
|
948
|
+
validator: audit.validator.id,
|
|
949
|
+
budget: audit.budget,
|
|
950
|
+
...(metricPlan ? { metric: metricPlan.expression } : {}),
|
|
951
|
+
verify: verifyPlan.command,
|
|
952
|
+
verify_status: verifyPlan.status,
|
|
953
|
+
verify_outcome: verifyPlan.outcome,
|
|
954
|
+
...(verifyWarning ? { verify_warning: verifyWarning } : {}),
|
|
955
|
+
task_text: taskText,
|
|
956
|
+
mission_room_receipt_path: reused
|
|
957
|
+
? (mission?.mission_room_receipt_path || null)
|
|
958
|
+
: writtenRoom.relativePath,
|
|
959
|
+
mission_room_name: reused
|
|
960
|
+
? (mission?.mission_room_name || null)
|
|
961
|
+
: (writtenRoom.room?.name || null),
|
|
962
|
+
mission_owner: reused ? (mission?.owner || routeOwner) : routeOwner,
|
|
963
|
+
...(reused ? { attached_to_existing_flight: true } : {}),
|
|
964
|
+
...(designBrief ? {
|
|
965
|
+
design_brief_paths: designBrief.paths,
|
|
966
|
+
design_audit_command: designBrief.audit_command,
|
|
967
|
+
} : {}),
|
|
968
|
+
...engineAuditFields(audit),
|
|
969
|
+
};
|
|
970
|
+
return { record, taskPayload, mission, verifyPlan };
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function appendDelegatedWishRecord(wish, audit, root, options = {}) {
|
|
974
|
+
const { record, verifyPlan } = startWishDelegation(wish, audit, root, options);
|
|
975
|
+
appendWishRecord(root, record);
|
|
976
|
+
return { record, verifyPlan };
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function delegateWish(wish, audit, root, asJson, options = {}) {
|
|
980
|
+
const { record, verifyPlan } = appendDelegatedWishRecord(wish, audit, root, options);
|
|
981
|
+
const payload = machineRecord(wish, 'delegated', audit, {
|
|
982
|
+
task_id: record.task_id,
|
|
983
|
+
mission_id: record.mission_id,
|
|
984
|
+
engine: record.engine,
|
|
985
|
+
...(record.verify_warning ? { warnings: [record.verify_warning] } : {}),
|
|
986
|
+
});
|
|
987
|
+
if (asJson) console.log(JSON.stringify(payload, null, 2));
|
|
988
|
+
else {
|
|
989
|
+
printGranted(options.taskText || wish.text, audit, {
|
|
990
|
+
...options,
|
|
991
|
+
verifyPlan,
|
|
992
|
+
engine: record.engine,
|
|
993
|
+
wish,
|
|
994
|
+
attachedToExistingFlight: record.attached_to_existing_flight === true,
|
|
995
|
+
});
|
|
996
|
+
if (record.verify_warning) console.log(record.verify_warning.message);
|
|
997
|
+
}
|
|
998
|
+
return 0;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function captureOnlyWish(wish, audit, root, asJson) {
|
|
1002
|
+
const now = stampIso();
|
|
1003
|
+
appendWishRecord(root, {
|
|
1004
|
+
id: wish.id,
|
|
1005
|
+
ts: now,
|
|
1006
|
+
text: wish.text,
|
|
1007
|
+
status: 'captured_no_mission',
|
|
1008
|
+
no_mission: true,
|
|
1009
|
+
parent_id: wish.parent_id || undefined,
|
|
1010
|
+
budget: audit ? audit.budget : inferBudgetTier(wish.text),
|
|
1011
|
+
questions: audit ? audit.questions : [],
|
|
1012
|
+
...engineAuditFields(audit),
|
|
1013
|
+
});
|
|
1014
|
+
const payload = machineRecord(wish, 'captured', audit, {
|
|
1015
|
+
engine: null,
|
|
1016
|
+
budget: audit ? audit.budget : inferBudgetTier(wish.text),
|
|
1017
|
+
});
|
|
1018
|
+
if (asJson) console.log(JSON.stringify(payload, null, 2));
|
|
1019
|
+
else {
|
|
1020
|
+
console.log(`Got it, wish ${wishTitle(wish)}.`);
|
|
1021
|
+
console.log('Saved it. I will not start work until you say so.');
|
|
1022
|
+
}
|
|
1023
|
+
return 0;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function builderBlockingQuestions(audit) {
|
|
1027
|
+
return (Array.isArray(audit && audit.questions) ? audit.questions : [])
|
|
1028
|
+
.filter((question) => !/^Which working (builder|reviewer) should /i.test(String(question || '').trim()));
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function builderSlice(text, audit, verifyPlan) {
|
|
1032
|
+
const exitCriteria = verifyPlan.status === 'derived'
|
|
1033
|
+
? verifyOutcomeText(verifyPlan)
|
|
1034
|
+
: 'caller confirms the outcome is done';
|
|
1035
|
+
return {
|
|
1036
|
+
outcome: String(text || '').trim(),
|
|
1037
|
+
exit_criteria: exitCriteria,
|
|
1038
|
+
verify: verifyPlan.command || '<add verify command>',
|
|
1039
|
+
budget: audit ? audit.budget : inferBudgetTier(text),
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function printBuilderSlice(wish, slice) {
|
|
1044
|
+
console.log(`Builder wish: ${quoteText(wish.text)}`);
|
|
1045
|
+
console.log(`Outcome: ${slice.outcome}`);
|
|
1046
|
+
console.log(`Exit criteria: ${slice.exit_criteria}.`);
|
|
1047
|
+
console.log(`Verify: ${slice.verify}`);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function builderWish(wish, audit, root, asJson) {
|
|
1051
|
+
const questions = builderBlockingQuestions(audit);
|
|
1052
|
+
if (questions.length) return askForInput(wish, { ...audit, questions }, root, asJson, { mode: 'builder' });
|
|
1053
|
+
const verifyPlan = deriveVerifyPlan(wish.text);
|
|
1054
|
+
const slice = builderSlice(wish.text, audit, verifyPlan);
|
|
1055
|
+
appendWishRecord(root, {
|
|
1056
|
+
id: wish.id,
|
|
1057
|
+
ts: stampIso(),
|
|
1058
|
+
text: wish.text,
|
|
1059
|
+
status: 'builder',
|
|
1060
|
+
mode: 'builder',
|
|
1061
|
+
parent_id: wish.parent_id || undefined,
|
|
1062
|
+
engine: null,
|
|
1063
|
+
validator: audit && audit.validator ? audit.validator.id : null,
|
|
1064
|
+
budget: slice.budget,
|
|
1065
|
+
verify: verifyPlan.command,
|
|
1066
|
+
verify_status: verifyPlan.status,
|
|
1067
|
+
verify_outcome: verifyPlan.outcome,
|
|
1068
|
+
builder_slice: slice,
|
|
1069
|
+
questions: audit ? audit.questions : [],
|
|
1070
|
+
...engineAuditFields(audit),
|
|
1071
|
+
});
|
|
1072
|
+
if (asJson) {
|
|
1073
|
+
console.log(JSON.stringify({
|
|
1074
|
+
wish_id: wish.id,
|
|
1075
|
+
status: 'builder',
|
|
1076
|
+
mode: 'builder',
|
|
1077
|
+
task_id: null,
|
|
1078
|
+
mission_id: null,
|
|
1079
|
+
engine: null,
|
|
1080
|
+
budget: slice.budget,
|
|
1081
|
+
questions: [],
|
|
1082
|
+
verify: verifyPlan.command,
|
|
1083
|
+
verify_status: verifyPlan.status,
|
|
1084
|
+
slice,
|
|
1085
|
+
}, null, 2));
|
|
1086
|
+
} else {
|
|
1087
|
+
printBuilderSlice(wish, slice);
|
|
1088
|
+
}
|
|
1089
|
+
return 0;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// Plain per-part reply: what started, what lives elsewhere, and one question per unclear part.
|
|
1093
|
+
function printDecomposed(wish, parts, delegatedParts, waitingParts) {
|
|
1094
|
+
console.log(`Got it, wish ${wishTitle(wish)}. It has ${parts.length} parts.`);
|
|
1095
|
+
delegatedParts.forEach((part) => {
|
|
1096
|
+
if (part.attached_to_existing_flight) {
|
|
1097
|
+
printGranted(part.text, null, {
|
|
1098
|
+
verifyPlan: {
|
|
1099
|
+
command: part.verify,
|
|
1100
|
+
outcome: part.verify_outcome,
|
|
1101
|
+
status: part.verify_status,
|
|
1102
|
+
},
|
|
1103
|
+
attachedToExistingFlight: true,
|
|
1104
|
+
});
|
|
1105
|
+
} else {
|
|
1106
|
+
console.log(`Starting now: ${part.text}.`);
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
const ownHome = waitingParts.filter((part) => part.reason && /not in this checkout/.test(part.reason));
|
|
1110
|
+
const unclear = waitingParts.filter((part) => !ownHome.includes(part));
|
|
1111
|
+
ownHome.forEach((part) => {
|
|
1112
|
+
console.log(`This part lives somewhere else, so I cannot do it from here: ${part.text}.`);
|
|
1113
|
+
});
|
|
1114
|
+
unclear.forEach((part) => {
|
|
1115
|
+
const question = (Array.isArray(part.questions) && part.questions[0])
|
|
1116
|
+
|| part.reason
|
|
1117
|
+
|| 'What should be different when this part comes true?';
|
|
1118
|
+
console.log(`One question about "${part.text}": ${question}`);
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function decomposeWish(wish, parts, root, asJson, options = {}) {
|
|
1123
|
+
const delegatedParts = [];
|
|
1124
|
+
const waitingParts = [];
|
|
1125
|
+
let firstAudit = null;
|
|
1126
|
+
for (const part of parts) {
|
|
1127
|
+
if (part.waiting_reason) {
|
|
1128
|
+
waitingParts.push({
|
|
1129
|
+
part: part.part,
|
|
1130
|
+
text: part.text,
|
|
1131
|
+
status: 'waiting',
|
|
1132
|
+
reason: part.waiting_reason,
|
|
1133
|
+
});
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
const audit = auditWish(part.text, root, options);
|
|
1137
|
+
if (!audit.ok) {
|
|
1138
|
+
waitingParts.push({
|
|
1139
|
+
part: part.part,
|
|
1140
|
+
text: part.text,
|
|
1141
|
+
status: 'waiting',
|
|
1142
|
+
reason: audit.questions[0] || 'needs a clearer scope',
|
|
1143
|
+
questions: audit.questions,
|
|
1144
|
+
});
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
if (!firstAudit) firstAudit = audit;
|
|
1148
|
+
const { record, verifyPlan } = startWishDelegation(wish, audit, root, {
|
|
1149
|
+
taskText: part.text,
|
|
1150
|
+
recordText: wish.text,
|
|
1151
|
+
verifyPlan: options.verifyPlan || deriveVerifyPlan(part.text),
|
|
1152
|
+
userVerifyProvided: options.verifyPlan && options.verifyPlan.status === 'explicit',
|
|
1153
|
+
});
|
|
1154
|
+
const delegated = {
|
|
1155
|
+
part: part.part,
|
|
1156
|
+
text: part.text,
|
|
1157
|
+
status: 'delegated',
|
|
1158
|
+
task_id: record.task_id,
|
|
1159
|
+
mission_id: record.mission_id,
|
|
1160
|
+
dispatched_at: record.dispatched_at,
|
|
1161
|
+
budget: record.budget,
|
|
1162
|
+
engine: record.engine,
|
|
1163
|
+
verify: record.verify,
|
|
1164
|
+
verify_status: record.verify_status,
|
|
1165
|
+
verify_outcome: record.verify_outcome,
|
|
1166
|
+
attached_to_existing_flight: record.attached_to_existing_flight === true,
|
|
1167
|
+
...engineAuditFields(audit),
|
|
1168
|
+
};
|
|
1169
|
+
delegatedParts.push(delegated);
|
|
1170
|
+
appendWishRecord(root, {
|
|
1171
|
+
...record,
|
|
1172
|
+
decomposed_part: part.part,
|
|
1173
|
+
parts_total: parts.length,
|
|
1174
|
+
verify_status: verifyPlan.status,
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
const statusParts = parts.map((part) => {
|
|
1178
|
+
const delegated = delegatedParts.find((item) => item.part === part.part);
|
|
1179
|
+
if (delegated) return { part: part.part, text: part.text, status: 'delegated' };
|
|
1180
|
+
const waiting = waitingParts.find((item) => item.part === part.part);
|
|
1181
|
+
return {
|
|
1182
|
+
part: part.part,
|
|
1183
|
+
text: part.text,
|
|
1184
|
+
status: 'waiting',
|
|
1185
|
+
reason: waiting ? waiting.reason : 'needs its own home',
|
|
1186
|
+
};
|
|
1187
|
+
});
|
|
1188
|
+
const firstDelegated = delegatedParts[0] || null;
|
|
1189
|
+
const record = {
|
|
1190
|
+
id: wish.id,
|
|
1191
|
+
ts: stampIso(),
|
|
1192
|
+
text: wish.text,
|
|
1193
|
+
status: 'decomposed',
|
|
1194
|
+
parent_id: wish.parent_id || undefined,
|
|
1195
|
+
task_id: firstDelegated ? firstDelegated.task_id : null,
|
|
1196
|
+
mission_id: firstDelegated ? firstDelegated.mission_id : null,
|
|
1197
|
+
engine: firstAudit && firstAudit.executor ? firstAudit.executor.id : null,
|
|
1198
|
+
validator: firstAudit && firstAudit.validator ? firstAudit.validator.id : null,
|
|
1199
|
+
budget: firstAudit ? firstAudit.budget : inferBudgetTier(wish.text),
|
|
1200
|
+
parts: statusParts,
|
|
1201
|
+
delegated_parts: delegatedParts,
|
|
1202
|
+
...engineAuditFields(firstAudit),
|
|
1203
|
+
out_of_scope_parts: waitingParts
|
|
1204
|
+
.filter((part) => part.reason && /not in this checkout/.test(part.reason))
|
|
1205
|
+
.map((part) => ({ ...part, status: 'waiting' })),
|
|
1206
|
+
waiting_parts: waitingParts,
|
|
1207
|
+
...(options.verifyPlan ? {
|
|
1208
|
+
verify: options.verifyPlan.command,
|
|
1209
|
+
verify_status: options.verifyPlan.status,
|
|
1210
|
+
verify_outcome: options.verifyPlan.outcome,
|
|
1211
|
+
} : {}),
|
|
1212
|
+
};
|
|
1213
|
+
appendWishRecord(root, record);
|
|
1214
|
+
const payload = {
|
|
1215
|
+
...machineRecord(wish, 'decomposed', firstAudit, {
|
|
1216
|
+
task_id: record.task_id,
|
|
1217
|
+
mission_id: record.mission_id,
|
|
1218
|
+
engine: record.engine,
|
|
1219
|
+
budget: record.budget,
|
|
1220
|
+
questions: [],
|
|
1221
|
+
}),
|
|
1222
|
+
parts: statusParts,
|
|
1223
|
+
delegated_parts: delegatedParts,
|
|
1224
|
+
out_of_scope_parts: record.out_of_scope_parts,
|
|
1225
|
+
};
|
|
1226
|
+
if (asJson) console.log(JSON.stringify(payload, null, 2));
|
|
1227
|
+
else printDecomposed(wish, parts, delegatedParts, waitingParts);
|
|
1228
|
+
return delegatedParts.length ? 0 : 1;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
function askForInput(wish, audit, root, asJson, extra = {}) {
|
|
1232
|
+
appendNeedsInputRecord(wish, audit, root, extra);
|
|
1233
|
+
const payload = machineRecord(wish, 'needs_input', audit);
|
|
1234
|
+
if (extra.mode) payload.mode = extra.mode;
|
|
1235
|
+
if (asJson) console.log(JSON.stringify(payload, null, 2));
|
|
1236
|
+
else printQuestion(wish, audit.questions[0]);
|
|
1237
|
+
return 1;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
function appendNeedsInputRecord(wish, audit, root, extra = {}) {
|
|
1241
|
+
appendWishRecord(root, {
|
|
1242
|
+
id: wish.id,
|
|
1243
|
+
ts: stampIso(),
|
|
1244
|
+
text: wish.text,
|
|
1245
|
+
status: 'needs_input',
|
|
1246
|
+
parent_id: wish.parent_id || undefined,
|
|
1247
|
+
questions: audit.questions,
|
|
1248
|
+
vague: !!audit.vague,
|
|
1249
|
+
missing_slots: audit.missing_slots || [],
|
|
1250
|
+
...extra,
|
|
1251
|
+
...engineAuditFields(audit),
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function intakeQuestionSubject(question) {
|
|
1256
|
+
const normalized = String(question || '').trim().replace(/\s+/g, ' ').toLowerCase();
|
|
1257
|
+
if (!normalized) return '';
|
|
1258
|
+
if (normalized.startsWith('which workspace, repo, file, or team member did you mean by ')) {
|
|
1259
|
+
return 'named-input';
|
|
1260
|
+
}
|
|
1261
|
+
const interpretationMarker = ' could mean ';
|
|
1262
|
+
const interpretationIndex = normalized.indexOf(interpretationMarker);
|
|
1263
|
+
if (interpretationIndex > 0) return `interpretation:${normalized.slice(0, interpretationIndex)}`;
|
|
1264
|
+
return `question:${normalized}`;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
function bestGuessAfterRepeatedQuestion(wish, audit, root) {
|
|
1268
|
+
if (!audit || audit.ok || !audit.executor || !audit.validator) return audit;
|
|
1269
|
+
const currentQuestion = Array.isArray(audit.questions) ? audit.questions[0] : '';
|
|
1270
|
+
const currentSubject = intakeQuestionSubject(currentQuestion);
|
|
1271
|
+
if (!currentSubject) return audit;
|
|
1272
|
+
const priorSubjects = new Set(readWishEvents(root)
|
|
1273
|
+
.filter((row) => row
|
|
1274
|
+
&& row.id === wish.id
|
|
1275
|
+
&& WAITING_INPUT_STATUSES.has(String(row.status || ''))
|
|
1276
|
+
&& Array.isArray(row.questions)
|
|
1277
|
+
&& row.questions.length)
|
|
1278
|
+
.map((row) => intakeQuestionSubject(row.questions[0]))
|
|
1279
|
+
.filter(Boolean));
|
|
1280
|
+
if (!priorSubjects.has(currentSubject)) return audit;
|
|
1281
|
+
return { ...audit, ok: true, questions: [] };
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function latestStatusFor(wish, latestEvent) {
|
|
1285
|
+
return String((latestEvent && latestEvent.status) || wish.status || '').trim();
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function latestEventHasGrantAnswer(latestEvent) {
|
|
1289
|
+
return eventAnswers(latestEvent).length > 0;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
function dispatchableWishReason(wish, latestEvent) {
|
|
1293
|
+
if (!wish || !wish.id || wish.dispatched_at || wish.no_mission || (latestEvent && latestEvent.no_mission)) return '';
|
|
1294
|
+
const status = latestStatusFor(wish, latestEvent);
|
|
1295
|
+
if (status === 'captured') return 'captured';
|
|
1296
|
+
if (WAITING_INPUT_STATUSES.has(status) && latestEventHasGrantAnswer(latestEvent)) return 'answered';
|
|
1297
|
+
return '';
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function waitingWishNeed(questions = []) {
|
|
1301
|
+
const first = (Array.isArray(questions) ? questions : [])
|
|
1302
|
+
.map((question) => String(question || '').trim())
|
|
1303
|
+
.find(Boolean) || 'Answer the open question and I will start the work.';
|
|
1304
|
+
if (/working builder/i.test(first)) return 'needs a working builder before it can start';
|
|
1305
|
+
if (/working reviewer/i.test(first)) return 'needs a working reviewer before it can start';
|
|
1306
|
+
if (/file or folder/i.test(first)) return 'needs the file or folder location before it can start';
|
|
1307
|
+
if (/workspace, repo, file, or team member/i.test(first)) return first.replace(/^Which /, 'needs the ');
|
|
1308
|
+
return first;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function waitingPartQuestion(part) {
|
|
1312
|
+
const questions = Array.isArray(part && part.questions) ? part.questions : [];
|
|
1313
|
+
return questions
|
|
1314
|
+
.map((question) => String(question || '').trim())
|
|
1315
|
+
.find(Boolean)
|
|
1316
|
+
|| String((part && (part.question || part.reason)) || '').trim();
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
function waitingPartQuestionTs(parent, part) {
|
|
1320
|
+
return String((part && (part.question_ts || part.asked_at || part.ts))
|
|
1321
|
+
|| (parent && (parent.question_ts || parent.asked_at || parent.ts))
|
|
1322
|
+
|| '').trim();
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
function firstAnswerableWaitingPart(wish) {
|
|
1326
|
+
if (!wish || String(wish.status || '') !== 'decomposed') return null;
|
|
1327
|
+
const waitingParts = Array.isArray(wish.waiting_parts) ? wish.waiting_parts : [];
|
|
1328
|
+
return waitingParts.find((part) => part
|
|
1329
|
+
&& String(part.text || '').trim()
|
|
1330
|
+
&& waitingPartQuestion(part));
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function decomposedPartWishId(parent, part) {
|
|
1334
|
+
const explicit = String((part && (part.wish_id || part.id || part.child_wish_id)) || '').trim();
|
|
1335
|
+
if (explicit) return explicit;
|
|
1336
|
+
const parentId = String((parent && parent.id) || 'wish').replace(/[^A-Za-z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
1337
|
+
const partNumber = Number(part && part.part);
|
|
1338
|
+
const suffix = Number.isInteger(partNumber) && partNumber > 0 ? String(partNumber) : 'waiting';
|
|
1339
|
+
return `${parentId || 'wish'}-part-${suffix}`;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
function decomposedAnswerTarget(wish, root) {
|
|
1343
|
+
const part = firstAnswerableWaitingPart(wish);
|
|
1344
|
+
if (!part) return null;
|
|
1345
|
+
const text = String(part.text || '').trim();
|
|
1346
|
+
const id = decomposedPartWishId(wish, part);
|
|
1347
|
+
const existing = readWishes(root).find((row) => row && row.id === id);
|
|
1348
|
+
const question = waitingPartQuestion(part);
|
|
1349
|
+
return {
|
|
1350
|
+
id,
|
|
1351
|
+
n: existing && existing.n ? existing.n : nextWishNumber(root),
|
|
1352
|
+
ts: waitingPartQuestionTs(wish, part) || wish.ts,
|
|
1353
|
+
text,
|
|
1354
|
+
status: 'needs_input',
|
|
1355
|
+
parent_id: wish.id,
|
|
1356
|
+
decomposed_part: part.part,
|
|
1357
|
+
questions: [question],
|
|
1358
|
+
answers: existing && Array.isArray(existing.answers) ? existing.answers : [],
|
|
1359
|
+
_answer_parent: wish,
|
|
1360
|
+
_answer_waiting_part: part,
|
|
1361
|
+
_answer_question_ts: waitingPartQuestionTs(wish, part) || wish.ts,
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
function answerQuestionTs(wish) {
|
|
1366
|
+
return String((wish && wish._answer_question_ts) || (wish && wish.question_ts) || (wish && wish.asked_at) || (wish && wish.ts) || '').trim();
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
function answerQuestionAgeMs(wish, now = Date.now()) {
|
|
1370
|
+
const parsed = Date.parse(answerQuestionTs(wish));
|
|
1371
|
+
if (!Number.isFinite(parsed)) return 0;
|
|
1372
|
+
return Math.max(0, now - parsed);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
function answerStaleNotice(wish, now = Date.now()) {
|
|
1376
|
+
const ageMs = answerQuestionAgeMs(wish, now);
|
|
1377
|
+
if (ageMs <= IMPLICIT_ANSWER_STALE_MS) return '';
|
|
1378
|
+
const displayWish = (wish && wish._answer_parent) || wish;
|
|
1379
|
+
const number = Number(displayWish && displayWish.n);
|
|
1380
|
+
const ref = Number.isInteger(number) && number > 0 ? `#${number}` : wishRef(displayWish);
|
|
1381
|
+
const label = wishLabel(displayWish).replace(/^#\d+\s*/, '').trim();
|
|
1382
|
+
const hours = Math.max(1, Math.floor(ageMs / HOUR_MS));
|
|
1383
|
+
return `The wish waiting on you is ${ref}${label ? ` ${label}` : ''}, asked ${hours} hours ago. If you mean that one, say: atris wish answer ${ref} "your words"`;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function waitingOperatorWishes(root = process.cwd()) {
|
|
1387
|
+
const latest = latestWishEventMap(root);
|
|
1388
|
+
return readWishes(root)
|
|
1389
|
+
.filter((wish) => {
|
|
1390
|
+
const latestEvent = latest.get(wish.id) || wish;
|
|
1391
|
+
const status = latestStatusFor(wish, latestEvent);
|
|
1392
|
+
return WAITING_INPUT_STATUSES.has(status) && !latestEventHasGrantAnswer(latestEvent) && !wish.dispatched_at;
|
|
1393
|
+
})
|
|
1394
|
+
.map((wish) => {
|
|
1395
|
+
const latestEvent = latest.get(wish.id) || wish;
|
|
1396
|
+
const questions = Array.isArray(latestEvent.questions) ? latestEvent.questions : (wish.questions || []);
|
|
1397
|
+
return {
|
|
1398
|
+
id: wish.id,
|
|
1399
|
+
text: String(wish.text || '').trim(),
|
|
1400
|
+
questions,
|
|
1401
|
+
need: waitingWishNeed(questions),
|
|
1402
|
+
first_ts: wish.first_ts || wish.ts || latestEvent.ts || null,
|
|
1403
|
+
};
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
function dispatchWishHeadlessly(wish, root, reason) {
|
|
1408
|
+
const auditText = reason === 'answered'
|
|
1409
|
+
? [wish.text, ...(wish.answers || [])].join(' ')
|
|
1410
|
+
: wish.text;
|
|
1411
|
+
const audited = auditWish(auditText, root, { originalText: wish.text });
|
|
1412
|
+
const audit = reason === 'answered'
|
|
1413
|
+
? bestGuessAfterRepeatedQuestion(wish, audited, root)
|
|
1414
|
+
: audited;
|
|
1415
|
+
if (!audit.ok) {
|
|
1416
|
+
appendNeedsInputRecord(wish, audit, root);
|
|
1417
|
+
return {
|
|
1418
|
+
id: wish.id,
|
|
1419
|
+
status: 'waiting_on_operator',
|
|
1420
|
+
no_executor: !audit.executor,
|
|
1421
|
+
no_validator: !audit.validator,
|
|
1422
|
+
questions: audit.questions,
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
const { record } = appendDelegatedWishRecord(wish, audit, root);
|
|
1426
|
+
return {
|
|
1427
|
+
id: wish.id,
|
|
1428
|
+
status: 'delegated',
|
|
1429
|
+
dispatched: true,
|
|
1430
|
+
task_id: record.task_id,
|
|
1431
|
+
mission_id: record.mission_id,
|
|
1432
|
+
engine: record.engine,
|
|
1433
|
+
dispatched_at: record.dispatched_at,
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
function fulfillLandedWishes(root = process.cwd(), options = {}) {
|
|
1438
|
+
const fulfilled = [];
|
|
1439
|
+
try {
|
|
1440
|
+
const taskDb = require('./task-db');
|
|
1441
|
+
const db = taskDb.open(options.dbPath);
|
|
1442
|
+
const events = readWishEvents(root);
|
|
1443
|
+
const latest = new Map();
|
|
1444
|
+
const terminal = new Set();
|
|
1445
|
+
for (const event of events) {
|
|
1446
|
+
const id = String(event && (event.id || event.wish_id) || '').trim();
|
|
1447
|
+
if (!id) continue;
|
|
1448
|
+
latest.set(id, event);
|
|
1449
|
+
const status = String(event.status || '').toLowerCase();
|
|
1450
|
+
const kind = String(event.kind || '').toLowerCase();
|
|
1451
|
+
if (['fulfilled', 'closed', 'complete', 'completed'].includes(status)
|
|
1452
|
+
|| ['fulfilled', 'closed'].includes(kind)) {
|
|
1453
|
+
terminal.add(id);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
for (const [id, event] of latest) {
|
|
1457
|
+
if (terminal.has(id) || event.status !== 'delegated' || !event.task_id) continue;
|
|
1458
|
+
const task = taskDb.getTask(db, event.task_id);
|
|
1459
|
+
if (!task || !['done', 'accepted'].includes(String(task.status || '').toLowerCase())) continue;
|
|
1460
|
+
const reviewAsk = `atris wish review ${id} "<your line>" [--score]`;
|
|
1461
|
+
const record = {
|
|
1462
|
+
id,
|
|
1463
|
+
kind: 'fulfilled',
|
|
1464
|
+
status: 'fulfilled',
|
|
1465
|
+
wish_id: id,
|
|
1466
|
+
task_id: event.task_id,
|
|
1467
|
+
mission_id: event.mission_id || null,
|
|
1468
|
+
ts: stampIso(),
|
|
1469
|
+
task_status: task.status,
|
|
1470
|
+
review_ask: reviewAsk,
|
|
1471
|
+
};
|
|
1472
|
+
appendWishRecord(root, record);
|
|
1473
|
+
terminal.add(id);
|
|
1474
|
+
fulfilled.push(record);
|
|
1475
|
+
}
|
|
1476
|
+
} catch {
|
|
1477
|
+
return [];
|
|
1478
|
+
}
|
|
1479
|
+
return fulfilled;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
function sweepWishes(root = process.cwd(), options = {}) {
|
|
1483
|
+
const limit = Number.isInteger(options.limit) && options.limit >= 0 ? options.limit : WISH_SWEEP_LIMIT;
|
|
1484
|
+
const fulfilledResults = fulfillLandedWishes(root, options);
|
|
1485
|
+
const latest = latestWishEventMap(root);
|
|
1486
|
+
const summary = {
|
|
1487
|
+
scanned: 0,
|
|
1488
|
+
dispatched: 0,
|
|
1489
|
+
fulfilled: fulfilledResults.length,
|
|
1490
|
+
fulfilled_results: fulfilledResults,
|
|
1491
|
+
capped: 0,
|
|
1492
|
+
waiting_on_operator: 0,
|
|
1493
|
+
skipped_no_executor: 0,
|
|
1494
|
+
results: [],
|
|
1495
|
+
};
|
|
1496
|
+
for (const wish of readWishes(root)) {
|
|
1497
|
+
const latestEvent = latest.get(wish.id) || wish;
|
|
1498
|
+
const reason = dispatchableWishReason(wish, latestEvent);
|
|
1499
|
+
if (!reason) continue;
|
|
1500
|
+
summary.scanned += 1;
|
|
1501
|
+
if (summary.dispatched >= limit) {
|
|
1502
|
+
summary.capped += 1;
|
|
1503
|
+
continue;
|
|
1504
|
+
}
|
|
1505
|
+
const result = dispatchWishHeadlessly(wish, root, reason);
|
|
1506
|
+
summary.results.push(result);
|
|
1507
|
+
if (result.dispatched) summary.dispatched += 1;
|
|
1508
|
+
if (result.no_executor) summary.skipped_no_executor += 1;
|
|
1509
|
+
}
|
|
1510
|
+
summary.waiting_on_operator = waitingOperatorWishes(root).length;
|
|
1511
|
+
return summary;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
function runCapturedWish(text, root = process.cwd(), options = {}) {
|
|
1515
|
+
const asJson = !!options.asJson;
|
|
1516
|
+
const noMission = !!options.noMission;
|
|
1517
|
+
const builderMode = String(options.asMode || '').trim() === 'builder';
|
|
1518
|
+
let engineOverride = '';
|
|
1519
|
+
try {
|
|
1520
|
+
engineOverride = validateEngineOverride(options.engineOverride, root);
|
|
1521
|
+
} catch (error) {
|
|
1522
|
+
console.error(error.message || String(error));
|
|
1523
|
+
return 2;
|
|
1524
|
+
}
|
|
1525
|
+
const ts = options.ts || stampIso();
|
|
1526
|
+
const wish = {
|
|
1527
|
+
id: options.wishId || wishId(text, ts),
|
|
1528
|
+
n: nextWishNumber(root),
|
|
1529
|
+
ts,
|
|
1530
|
+
text,
|
|
1531
|
+
status: 'captured',
|
|
1532
|
+
};
|
|
1533
|
+
if (options.parentId) wish.parent_id = String(options.parentId);
|
|
1534
|
+
if (engineOverride) wish.requested_engine = engineOverride;
|
|
1535
|
+
if (options.metric && options.metric.ok) wish.metric = options.metric.expression;
|
|
1536
|
+
captureWishToJournal(text, root);
|
|
1537
|
+
appendWishRecord(root, wish);
|
|
1538
|
+
if (typeof options.afterCapture === 'function') options.afterCapture(wish);
|
|
1539
|
+
const taskText = wishBodyText(text);
|
|
1540
|
+
const verifyPlan = options.verifyPlan || deriveVerifyPlan(text);
|
|
1541
|
+
const decompositionVerifyPlan = options.verifyPlan || (verifyPlan.status === 'explicit' ? verifyPlan : null);
|
|
1542
|
+
const audit = auditWish(taskText, root, { engineOverride });
|
|
1543
|
+
if (builderMode) return builderWish(wish, audit, root, asJson);
|
|
1544
|
+
const parts = options.singleTask ? null : analyzeWishParts(text, root);
|
|
1545
|
+
if (parts && noMission) return captureOnlyWish(wish, null, root, asJson);
|
|
1546
|
+
if (parts) return decomposeWish(wish, parts, root, asJson, {
|
|
1547
|
+
engineOverride,
|
|
1548
|
+
...(decompositionVerifyPlan ? { verifyPlan: decompositionVerifyPlan } : {}),
|
|
1549
|
+
});
|
|
1550
|
+
if (!audit.ok) return askForInput(wish, audit, root, asJson);
|
|
1551
|
+
if (noMission) return captureOnlyWish(wish, audit, root, asJson);
|
|
1552
|
+
return delegateWish(wish, audit, root, asJson, {
|
|
1553
|
+
taskText,
|
|
1554
|
+
recordText: wish.text,
|
|
1555
|
+
...(options.metric ? { metric: options.metric } : {}),
|
|
1556
|
+
verifyPlan,
|
|
1557
|
+
userVerifyProvided: options.userVerifyProvided === true || verifyPlan.status === 'explicit',
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
function inheritedEngineOverride(parent, explicitEngineOverride = '') {
|
|
1562
|
+
const explicit = String(explicitEngineOverride || '').trim();
|
|
1563
|
+
if (explicit) return explicit;
|
|
1564
|
+
return String(parent && (parent.requested_engine || parent.engine_override) || '').trim();
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
function runAgainWish(positionals, root = process.cwd(), options = {}) {
|
|
1568
|
+
const parentId = String(positionals[1] || '').trim();
|
|
1569
|
+
const text = positionals.slice(2).join(' ').trim();
|
|
1570
|
+
if (!parentId || !text) {
|
|
1571
|
+
console.error('wish again needs a parent id and tweak text.');
|
|
1572
|
+
return 2;
|
|
1573
|
+
}
|
|
1574
|
+
const parent = readWishes(root).find((wish) => wishMatchesRef(wish, parentId));
|
|
1575
|
+
if (!parent) {
|
|
1576
|
+
console.error(`No wish found with id ${parentId}.`);
|
|
1577
|
+
return 2;
|
|
1578
|
+
}
|
|
1579
|
+
const ts = stampIso();
|
|
1580
|
+
const nextId = wishId(text, ts);
|
|
1581
|
+
const engineOverride = inheritedEngineOverride(parent, options.engineOverride);
|
|
1582
|
+
const afterCapture = options.asJson
|
|
1583
|
+
? null
|
|
1584
|
+
: () => console.log(`Following up on wish ${wishTitle(parent)}.`);
|
|
1585
|
+
return runCapturedWish(text, root, {
|
|
1586
|
+
...options,
|
|
1587
|
+
ts,
|
|
1588
|
+
wishId: nextId,
|
|
1589
|
+
parentId: parent.id,
|
|
1590
|
+
engineOverride,
|
|
1591
|
+
afterCapture,
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// Record the answer against the wish, then dispatch it or ask the next single question.
|
|
1596
|
+
function applyAnswer(wish, answer, root, options = {}) {
|
|
1597
|
+
const asJson = !!options.asJson;
|
|
1598
|
+
const noMission = !!options.noMission;
|
|
1599
|
+
let engineOverride = '';
|
|
1600
|
+
try {
|
|
1601
|
+
engineOverride = validateEngineOverride(options.engineOverride, root);
|
|
1602
|
+
} catch (error) {
|
|
1603
|
+
console.error(error.message || String(error));
|
|
1604
|
+
return 2;
|
|
1605
|
+
}
|
|
1606
|
+
appendWishRecord(root, {
|
|
1607
|
+
id: wish.id,
|
|
1608
|
+
n: wish.n || undefined,
|
|
1609
|
+
ts: stampIso(),
|
|
1610
|
+
text: wish.text,
|
|
1611
|
+
status: 'needs_input',
|
|
1612
|
+
parent_id: wish.parent_id || undefined,
|
|
1613
|
+
decomposed_part: wish.decomposed_part || undefined,
|
|
1614
|
+
answer,
|
|
1615
|
+
});
|
|
1616
|
+
const answeredWish = {
|
|
1617
|
+
...wish,
|
|
1618
|
+
answers: [...(wish.answers || []), answer],
|
|
1619
|
+
};
|
|
1620
|
+
const auditText = [wish.text, ...(answeredWish.answers || [])].join(' ');
|
|
1621
|
+
const audited = auditWish(auditText, root, { engineOverride, originalText: wish.text });
|
|
1622
|
+
const audit = bestGuessAfterRepeatedQuestion(answeredWish, audited, root);
|
|
1623
|
+
if (!audit.ok) return askForInput(answeredWish, audit, root, asJson);
|
|
1624
|
+
if (noMission) return captureOnlyWish(answeredWish, audit, root, asJson);
|
|
1625
|
+
return delegateWish(answeredWish, audit, root, asJson, {});
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
function sameWaitingPart(left, right) {
|
|
1629
|
+
if (!left || !right) return false;
|
|
1630
|
+
const leftPart = Number(left.part);
|
|
1631
|
+
const rightPart = Number(right.part);
|
|
1632
|
+
if (Number.isInteger(leftPart) && Number.isInteger(rightPart) && leftPart === rightPart) return true;
|
|
1633
|
+
return String(left.text || '').trim() === String(right.text || '').trim();
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
function markDecomposedPartAnswered(parent, part, child, root) {
|
|
1637
|
+
const now = stampIso();
|
|
1638
|
+
const questionTs = waitingPartQuestionTs(parent, part);
|
|
1639
|
+
const waitingParts = Array.isArray(parent.waiting_parts) ? parent.waiting_parts : [];
|
|
1640
|
+
const remainingWaitingParts = waitingParts
|
|
1641
|
+
.filter((waitingPart) => !sameWaitingPart(waitingPart, part))
|
|
1642
|
+
.map((waitingPart) => {
|
|
1643
|
+
const ts = waitingPartQuestionTs(parent, waitingPart);
|
|
1644
|
+
return ts ? { ...waitingPart, question_ts: ts } : waitingPart;
|
|
1645
|
+
});
|
|
1646
|
+
const answeredPart = {
|
|
1647
|
+
...part,
|
|
1648
|
+
status: 'answered',
|
|
1649
|
+
child_wish_id: child.id,
|
|
1650
|
+
answered_at: now,
|
|
1651
|
+
...(questionTs ? { question_ts: questionTs } : {}),
|
|
1652
|
+
};
|
|
1653
|
+
const parts = Array.isArray(parent.parts)
|
|
1654
|
+
? parent.parts.map((row) => (sameWaitingPart(row, part)
|
|
1655
|
+
? { ...row, status: 'answered', child_wish_id: child.id }
|
|
1656
|
+
: row))
|
|
1657
|
+
: undefined;
|
|
1658
|
+
appendWishRecord(root, {
|
|
1659
|
+
id: parent.id,
|
|
1660
|
+
n: parent.n || undefined,
|
|
1661
|
+
ts: now,
|
|
1662
|
+
text: parent.text,
|
|
1663
|
+
status: 'decomposed',
|
|
1664
|
+
parent_id: parent.parent_id || undefined,
|
|
1665
|
+
task_id: parent.task_id || null,
|
|
1666
|
+
mission_id: parent.mission_id || null,
|
|
1667
|
+
engine: parent.engine || null,
|
|
1668
|
+
validator: parent.validator || null,
|
|
1669
|
+
budget: parent.budget || inferBudgetTier(parent.text),
|
|
1670
|
+
...(parts ? { parts } : {}),
|
|
1671
|
+
delegated_parts: Array.isArray(parent.delegated_parts) ? parent.delegated_parts : [],
|
|
1672
|
+
out_of_scope_parts: Array.isArray(parent.out_of_scope_parts) ? parent.out_of_scope_parts : [],
|
|
1673
|
+
waiting_parts: remainingWaitingParts,
|
|
1674
|
+
answered_parts: [...(Array.isArray(parent.answered_parts) ? parent.answered_parts : []), answeredPart],
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
function applySelectedAnswer(wish, answer, root, options = {}) {
|
|
1679
|
+
if (wish && wish._answer_parent && wish._answer_waiting_part) {
|
|
1680
|
+
const child = {
|
|
1681
|
+
id: wish.id,
|
|
1682
|
+
n: wish.n,
|
|
1683
|
+
ts: stampIso(),
|
|
1684
|
+
text: wish.text,
|
|
1685
|
+
status: 'needs_input',
|
|
1686
|
+
parent_id: wish._answer_parent.id,
|
|
1687
|
+
decomposed_part: wish._answer_waiting_part.part,
|
|
1688
|
+
questions: wish.questions,
|
|
1689
|
+
answers: wish.answers,
|
|
1690
|
+
};
|
|
1691
|
+
const code = applyAnswer(child, answer, root, options);
|
|
1692
|
+
if (code !== 2) markDecomposedPartAnswered(wish._answer_parent, wish._answer_waiting_part, child, root);
|
|
1693
|
+
return code;
|
|
1694
|
+
}
|
|
1695
|
+
return applyAnswer(wish, answer, root, options);
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
function waitingAnswerWishes(root = process.cwd()) {
|
|
1699
|
+
const waiting = [];
|
|
1700
|
+
for (const wish of readWishes(root)) {
|
|
1701
|
+
if (!wish || !wish.id) continue;
|
|
1702
|
+
if (WAITING_INPUT_STATUSES.has(String(wish.status || '')) && !wish.dispatched_at) {
|
|
1703
|
+
waiting.push({
|
|
1704
|
+
...wish,
|
|
1705
|
+
_answer_question_ts: answerQuestionTs(wish),
|
|
1706
|
+
});
|
|
1707
|
+
continue;
|
|
1708
|
+
}
|
|
1709
|
+
const decomposed = decomposedAnswerTarget(wish, root);
|
|
1710
|
+
if (decomposed) waiting.push(decomposed);
|
|
1711
|
+
}
|
|
1712
|
+
return waiting;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
// Only #n and wish ids count as explicit targets here. Bare numbers stay part of
|
|
1716
|
+
// the answer text so "3 login screens" never lands on the wrong wish.
|
|
1717
|
+
function explicitAnswerRef(value) {
|
|
1718
|
+
const text = String(value || '').trim();
|
|
1719
|
+
return /^#\d+$/.test(text) || looksLikeWishId(text);
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
// atris wish answer "your words" — answers the pending question of the wish
|
|
1723
|
+
// that is waiting on you, no list numbers involved.
|
|
1724
|
+
function answerWish(positionals, root = process.cwd(), options = {}) {
|
|
1725
|
+
const parts = positionals.slice(1).map(String).filter((value) => value.trim());
|
|
1726
|
+
if (!parts.length) {
|
|
1727
|
+
console.error('wish answer needs your answer in plain words.');
|
|
1728
|
+
return 2;
|
|
1729
|
+
}
|
|
1730
|
+
const waiting = waitingAnswerWishes(root);
|
|
1731
|
+
if (!waiting.length) {
|
|
1732
|
+
console.error('No wish is waiting on an answer.');
|
|
1733
|
+
return 2;
|
|
1734
|
+
}
|
|
1735
|
+
let wish = null;
|
|
1736
|
+
let answerParts = parts;
|
|
1737
|
+
let hasExplicitRef = false;
|
|
1738
|
+
if (parts.length > 1 && explicitAnswerRef(parts[0])) {
|
|
1739
|
+
hasExplicitRef = true;
|
|
1740
|
+
wish = waiting.find((row) => answerWishMatchesRef(row, parts[0]));
|
|
1741
|
+
if (!wish) {
|
|
1742
|
+
console.error(`No wish waiting on an answer matches ${parts[0]}.`);
|
|
1743
|
+
return 2;
|
|
1744
|
+
}
|
|
1745
|
+
answerParts = parts.slice(1);
|
|
1746
|
+
}
|
|
1747
|
+
if (!wish) {
|
|
1748
|
+
wish = waiting.reduce((latest, row) => (String(answerQuestionTs(row)) >= String(answerQuestionTs(latest)) ? row : latest), waiting[0]);
|
|
1749
|
+
}
|
|
1750
|
+
const answer = answerParts.join(' ').trim();
|
|
1751
|
+
if (!answer) {
|
|
1752
|
+
console.error('wish answer needs your answer in plain words.');
|
|
1753
|
+
return 2;
|
|
1754
|
+
}
|
|
1755
|
+
if (!hasExplicitRef) {
|
|
1756
|
+
const notice = answerStaleNotice(wish);
|
|
1757
|
+
if (notice) {
|
|
1758
|
+
console.error(notice);
|
|
1759
|
+
return 2;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
return applySelectedAnswer(wish, answer, root, options);
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
function grantWish(positionals, root = process.cwd(), options = {}) {
|
|
1766
|
+
const asJson = !!options.asJson;
|
|
1767
|
+
const number = Number(positionals[1]);
|
|
1768
|
+
const answer = positionals.slice(2).join(' ').trim();
|
|
1769
|
+
if (!Number.isInteger(number) || number <= 0 || !answer) {
|
|
1770
|
+
console.error('wish grant needs a list number and an answer.');
|
|
1771
|
+
return 2;
|
|
1772
|
+
}
|
|
1773
|
+
const waiting = listedWishes(root).visible;
|
|
1774
|
+
const wish = waiting[number - 1];
|
|
1775
|
+
if (!wish) {
|
|
1776
|
+
console.error('No wish is waiting at that number.');
|
|
1777
|
+
return 2;
|
|
1778
|
+
}
|
|
1779
|
+
if (!WAITING_INPUT_STATUSES.has(String(wish.status || ''))) {
|
|
1780
|
+
console.error('That wish is not waiting on an answer.');
|
|
1781
|
+
return 2;
|
|
1782
|
+
}
|
|
1783
|
+
const vagueFlagged = wish.vague === true
|
|
1784
|
+
|| (Array.isArray(wish.missing_slots) && wish.missing_slots.length > 0)
|
|
1785
|
+
|| (Array.isArray(wish.questions) && wish.questions.some((question) => /\b(different|outcome|Who is|What part)\b/i.test(String(question))));
|
|
1786
|
+
if (vagueFlagged && !sharesMeaningfulWords(wish.text, answer)) {
|
|
1787
|
+
const notice = 'This answer may be for a different wish, so I did not start it.';
|
|
1788
|
+
if (asJson) {
|
|
1789
|
+
const payload = machineRecord(wish, 'needs_input', {
|
|
1790
|
+
executor: null,
|
|
1791
|
+
budget: wish.budget || inferBudgetTier(wish.text),
|
|
1792
|
+
questions: wish.questions || [],
|
|
1793
|
+
}, {
|
|
1794
|
+
engine: wish.engine || null,
|
|
1795
|
+
});
|
|
1796
|
+
console.log(JSON.stringify({
|
|
1797
|
+
...payload,
|
|
1798
|
+
mismatch: true,
|
|
1799
|
+
notice,
|
|
1800
|
+
wish_text: wish.text,
|
|
1801
|
+
}, null, 2));
|
|
1802
|
+
} else {
|
|
1803
|
+
console.log(`That answer sounds like a different wish, so I did not start wish ${wishTitle(wish)}.`);
|
|
1804
|
+
printList(root);
|
|
1805
|
+
}
|
|
1806
|
+
return 1;
|
|
1807
|
+
}
|
|
1808
|
+
return applyAnswer(wish, answer, root, options);
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
module.exports = {
|
|
1812
|
+
REVIEW_WORD_SCORES,
|
|
1813
|
+
answerWish,
|
|
1814
|
+
appendDelegatedWishRecord,
|
|
1815
|
+
appendNeedsInputRecord,
|
|
1816
|
+
askForInput,
|
|
1817
|
+
captureOnlyWish,
|
|
1818
|
+
captureWishToJournal,
|
|
1819
|
+
delegateWish,
|
|
1820
|
+
dispatchWishHeadlessly,
|
|
1821
|
+
fulfillLandedWishes,
|
|
1822
|
+
grantWish,
|
|
1823
|
+
metricVerifierCommand,
|
|
1824
|
+
listedWishes,
|
|
1825
|
+
openWishes,
|
|
1826
|
+
parseMetricExpression,
|
|
1827
|
+
printBoard,
|
|
1828
|
+
printList,
|
|
1829
|
+
printReviewNudges,
|
|
1830
|
+
printRewards,
|
|
1831
|
+
reviewNudgeWishes,
|
|
1832
|
+
reviewWordScore,
|
|
1833
|
+
reviewWish,
|
|
1834
|
+
runAgainWish,
|
|
1835
|
+
runCapturedWish,
|
|
1836
|
+
sayWish,
|
|
1837
|
+
startWishDelegation,
|
|
1838
|
+
sweepWishes,
|
|
1839
|
+
waitingOperatorWishes,
|
|
1840
|
+
};
|