atris 3.73.1 → 3.74.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/atris.js +85 -82
- package/commands/aeo.js +2 -2
- package/commands/align.js +10 -19
- package/commands/auth.js +1 -1
- package/commands/autopilot.js +4 -8
- package/commands/avail.js +36 -7
- package/commands/computer.js +3 -1
- package/commands/console.js +1 -1
- package/commands/context-sync.js +1 -60
- package/commands/fleet.js +0 -1
- package/commands/lifecycle.js +17 -6
- package/commands/log-sync.js +1 -1
- package/commands/member.js +158 -18
- package/commands/mission.js +157 -69
- package/commands/now.js +0 -7
- package/commands/plugin.js +1 -1
- package/commands/pull.js +1 -1
- package/commands/push.js +0 -1
- package/commands/radar.js +0 -4
- package/commands/recap.js +25 -1
- package/commands/report.js +0 -4
- package/commands/review.js +1 -1
- package/commands/setup.js +6 -2
- package/commands/slop.js +8 -2
- package/commands/task.js +0 -522
- package/commands/terminal.js +1 -1
- package/commands/youtube.js +4 -6
- package/commands/zen.js +4 -4
- package/lib/agent-today.js +412 -0
- package/lib/autoland.js +0 -8
- package/lib/bench/runner.js +0 -4
- package/lib/chat-log-scan.js +0 -28
- package/lib/cli-scope.js +4 -2
- package/lib/engine-registry.js +0 -4
- package/lib/falsifier-probe.js +2 -1
- package/lib/file-ops.js +6 -1
- package/lib/first-minute.js +2 -0
- package/lib/gm-bet.js +125 -30
- package/lib/gm-phone-page.js +398 -99
- package/lib/gm-phone.js +30 -3
- package/lib/handoff-wake.js +180 -0
- package/lib/manifest.js +0 -10
- package/lib/member-switches.js +0 -11
- package/lib/member-up.js +18 -3
- package/lib/mission-protected-lane.js +38 -6
- package/lib/review-integrity.js +0 -4
- package/lib/spawn-env.js +92 -3
- package/lib/task-proof.js +2 -1
- package/lib/task-receipt.js +3 -2
- package/lib/yes-lock.js +633 -81
- package/package.json +1 -1
package/commands/mission.js
CHANGED
|
@@ -38,6 +38,7 @@ const {
|
|
|
38
38
|
resolveEngineForRoleWithPreference,
|
|
39
39
|
engineFailureHealthStatus,
|
|
40
40
|
engineStallReason,
|
|
41
|
+
engineRegistryView,
|
|
41
42
|
recordEngineRunHealth,
|
|
42
43
|
setEngineHealth,
|
|
43
44
|
} = require('../lib/engine-registry');
|
|
@@ -1905,15 +1906,58 @@ function recordMemberHandoff(fromMember, toMember, sliceSummary, root = process.
|
|
|
1905
1906
|
next_command: `atris member go ${to}`,
|
|
1906
1907
|
created_at: now,
|
|
1907
1908
|
};
|
|
1909
|
+
const handoffsFile = path.join(parentRoot, '.atris', 'state', 'member-handoffs.jsonl');
|
|
1910
|
+
try {
|
|
1911
|
+
withHandoffsLock(handoffsFile, () => fs.appendFileSync(handoffsFile, `${JSON.stringify(handoff)}\n`, 'utf8'));
|
|
1912
|
+
} catch {
|
|
1913
|
+
return null;
|
|
1914
|
+
}
|
|
1908
1915
|
try {
|
|
1909
|
-
const handoffsFile = path.join(parentRoot, '.atris', 'state', 'member-handoffs.jsonl');
|
|
1910
|
-
fs.mkdirSync(path.dirname(handoffsFile), { recursive: true });
|
|
1911
|
-
fs.appendFileSync(handoffsFile, `${JSON.stringify(handoff)}\n`, 'utf8');
|
|
1912
1916
|
autoQueueMemberNextPush(to, `${from} handoff: ${cleanSummary}`, `Baton pass from ${from}`, parentRoot);
|
|
1913
1917
|
} catch {}
|
|
1914
1918
|
return handoff;
|
|
1915
1919
|
}
|
|
1916
1920
|
|
|
1921
|
+
// Every write to member-handoffs.jsonl (append or whole-file rewrite) holds
|
|
1922
|
+
// one lock file beside it, so concurrent handoffs never lose a row.
|
|
1923
|
+
// (Codex review round 3 on #1483.)
|
|
1924
|
+
function withHandoffsLock(handoffsFile, fn) {
|
|
1925
|
+
fs.mkdirSync(path.dirname(handoffsFile), { recursive: true });
|
|
1926
|
+
const lockFile = path.join(path.dirname(handoffsFile), 'member-handoffs.lock');
|
|
1927
|
+
return require('../lib/handoff-wake').withFileLock(lockFile, fn, { staleMs: 60 * 1000, waitMs: 30 * 1000 });
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
function rewriteHandoffs(handoffsFile, lines) {
|
|
1931
|
+
const tmp = `${handoffsFile}.${process.pid}.tmp`;
|
|
1932
|
+
fs.writeFileSync(tmp, `${lines.join('\n')}\n`, 'utf8');
|
|
1933
|
+
fs.renameSync(tmp, handoffsFile);
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
// Merge `patch` into the newest handoff row with this id. Returns the
|
|
1937
|
+
// updated row, or null when it is not on file.
|
|
1938
|
+
function updateMemberHandoff(handoffId, patch, root = process.cwd()) {
|
|
1939
|
+
const id = String(handoffId || '');
|
|
1940
|
+
if (!id || !patch) return null;
|
|
1941
|
+
const handoffsFile = path.join(resolveMemberWorkspaceRoot(root), '.atris', 'state', 'member-handoffs.jsonl');
|
|
1942
|
+
try {
|
|
1943
|
+
return withHandoffsLock(handoffsFile, () => {
|
|
1944
|
+
const lines = fs.readFileSync(handoffsFile, 'utf8').split(/\r?\n/).filter((l) => l.trim());
|
|
1945
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
1946
|
+
let row = null;
|
|
1947
|
+
try { row = JSON.parse(lines[i]); } catch {}
|
|
1948
|
+
if (!row || row.id !== id) continue;
|
|
1949
|
+
const updated = { ...row, ...patch };
|
|
1950
|
+
lines[i] = JSON.stringify(updated);
|
|
1951
|
+
rewriteHandoffs(handoffsFile, lines);
|
|
1952
|
+
return updated;
|
|
1953
|
+
}
|
|
1954
|
+
return null;
|
|
1955
|
+
});
|
|
1956
|
+
} catch {
|
|
1957
|
+
return null;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1917
1961
|
function closeMemberHandoffsForReceiver(receiverMember, root = process.cwd()) {
|
|
1918
1962
|
const receiver = String(receiverMember || '').trim().toLowerCase();
|
|
1919
1963
|
if (!receiver) return 0;
|
|
@@ -1921,28 +1965,30 @@ function closeMemberHandoffsForReceiver(receiverMember, root = process.cwd()) {
|
|
|
1921
1965
|
const handoffsFile = path.join(parentRoot, '.atris', 'state', 'member-handoffs.jsonl');
|
|
1922
1966
|
if (!fs.existsSync(handoffsFile)) return 0;
|
|
1923
1967
|
try {
|
|
1924
|
-
|
|
1925
|
-
let closedCount = 0;
|
|
1926
|
-
const now = stampIso();
|
|
1927
|
-
const updatedLines = rawLines.map((line) => {
|
|
1928
|
-
try {
|
|
1929
|
-
const row = JSON.parse(line);
|
|
1930
|
-
if (row && String(row.to || '').toLowerCase() === receiver && (row.status || 'open') === 'open') {
|
|
1931
|
-
closedCount++;
|
|
1932
|
-
return JSON.stringify({ ...row, status: 'closed', closed_at: now });
|
|
1933
|
-
}
|
|
1934
|
-
} catch {}
|
|
1935
|
-
return line;
|
|
1936
|
-
});
|
|
1937
|
-
if (closedCount > 0) {
|
|
1938
|
-
fs.writeFileSync(handoffsFile, `${updatedLines.join('\n')}\n`, 'utf8');
|
|
1939
|
-
}
|
|
1940
|
-
return closedCount;
|
|
1968
|
+
return withHandoffsLock(handoffsFile, () => closeHandoffRows(handoffsFile, receiver));
|
|
1941
1969
|
} catch {
|
|
1942
1970
|
return 0;
|
|
1943
1971
|
}
|
|
1944
1972
|
}
|
|
1945
1973
|
|
|
1974
|
+
function closeHandoffRows(handoffsFile, receiver) {
|
|
1975
|
+
const rawLines = fs.readFileSync(handoffsFile, 'utf8').split(/\r?\n/).filter((l) => l.trim());
|
|
1976
|
+
let closedCount = 0;
|
|
1977
|
+
const now = stampIso();
|
|
1978
|
+
const updatedLines = rawLines.map((line) => {
|
|
1979
|
+
try {
|
|
1980
|
+
const row = JSON.parse(line);
|
|
1981
|
+
if (row && String(row.to || '').toLowerCase() === receiver && (row.status || 'open') === 'open') {
|
|
1982
|
+
closedCount++;
|
|
1983
|
+
return JSON.stringify({ ...row, status: 'closed', closed_at: now });
|
|
1984
|
+
}
|
|
1985
|
+
} catch {}
|
|
1986
|
+
return line;
|
|
1987
|
+
});
|
|
1988
|
+
if (closedCount > 0) rewriteHandoffs(handoffsFile, updatedLines);
|
|
1989
|
+
return closedCount;
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1946
1992
|
function memberMissionFile(owner, root = process.cwd()) {
|
|
1947
1993
|
const dir = memberDir(owner, root);
|
|
1948
1994
|
if (!dir) return null;
|
|
@@ -3217,6 +3263,7 @@ function missionFromArgs(args) {
|
|
|
3217
3263
|
'--task',
|
|
3218
3264
|
'--ask',
|
|
3219
3265
|
'--yes-lock',
|
|
3266
|
+
'--yes-lock-parent',
|
|
3220
3267
|
'--campaign',
|
|
3221
3268
|
'--reserve',
|
|
3222
3269
|
'--mission-id',
|
|
@@ -3260,7 +3307,18 @@ function missionFromArgs(args) {
|
|
|
3260
3307
|
const taskIds = readRepeatedFlag(args, '--task');
|
|
3261
3308
|
// MEMBER.md approval-required becomes an open human ask the owner must clear.
|
|
3262
3309
|
const humanAsks = [...readRepeatedFlag(args, '--ask'), ...memberApprovalAsks(owner, process.cwd(), objective)];
|
|
3263
|
-
|
|
3310
|
+
// The seal is never read from text a caller passes through: two
|
|
3311
|
+
// --yes-lock words (one could be the objective) refuse the start, and a
|
|
3312
|
+
// run woken by a handoff carries its seal in the environment, which every
|
|
3313
|
+
// job it starts inherits. (Codex review round 6 on #1483.)
|
|
3314
|
+
if (args.filter(isYesLockWord).length > 1) {
|
|
3315
|
+
exitMissionError('mission start got more than one --yes-lock, so it is not clear which seal applies; nothing started', 2, wantsJson(args));
|
|
3316
|
+
}
|
|
3317
|
+
const handoffSeal = String(process.env.ATRIS_HANDOFF_YES_LOCK || '').trim();
|
|
3318
|
+
const yesLocks = readYesLockSeal(handoffSeal || readFlag(args, '--yes-lock', ''), wantsJson(args));
|
|
3319
|
+
const yesLockParent = yesLocks.length
|
|
3320
|
+
? String((handoffSeal ? process.env.ATRIS_HANDOFF_YES_LOCK_PARENT : readFlag(args, '--yes-lock-parent', '')) || '').trim()
|
|
3321
|
+
: '';
|
|
3264
3322
|
const drive = missionDrive(readFlag(args, '--destination', ''));
|
|
3265
3323
|
const alwaysOn = hasFlag(args, '--always-on');
|
|
3266
3324
|
const xpTaskEnabled = hasFlag(args, '--xp-task') || hasFlag(args, '--agent-xp');
|
|
@@ -3299,6 +3357,9 @@ function missionFromArgs(args) {
|
|
|
3299
3357
|
human_asks: humanAsks,
|
|
3300
3358
|
// the sealed owner steps: the guard holds what each locks until the owner says yes
|
|
3301
3359
|
...(yesLocks.length ? { yes_locks: { steps: yesLocks, sealed_at: stampIso() } } : {}),
|
|
3360
|
+
// a run woken by a handoff: the job it came from, only to show its yes
|
|
3361
|
+
// cards there (lib/yes-lock.js handoffYesCards)
|
|
3362
|
+
...(yesLockParent ? { yes_lock_parent: yesLockParent } : {}),
|
|
3302
3363
|
...(campaignId ? { campaign_id: campaignId } : {}),
|
|
3303
3364
|
...(drive ? { drive } : {}),
|
|
3304
3365
|
next_action: verifier ? 'run verifier with `atris mission tick <id> --verify`' : 'define verifier or run next task',
|
|
@@ -3319,7 +3380,12 @@ function missionFromArgs(args) {
|
|
|
3319
3380
|
|
|
3320
3381
|
// --yes-lock '[{"step":3,"text":"...","categories":["publish"]}]': the owner
|
|
3321
3382
|
// steps a job may not do before the owner says yes. Bad input stops the start.
|
|
3322
|
-
const YES_LOCK_CATEGORIES = new Set(['publish', 'tag', 'post', 'send', 'merge', 'deploy']);
|
|
3383
|
+
const YES_LOCK_CATEGORIES = new Set(['publish', 'tag', 'post', 'send', 'merge', 'deploy', 'spend']);
|
|
3384
|
+
function isYesLockWord(arg) {
|
|
3385
|
+
const text = String(arg || '').trim();
|
|
3386
|
+
return text === '--yes-lock' || text.startsWith('--yes-lock=');
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3323
3389
|
function readYesLockSeal(raw, asJson) {
|
|
3324
3390
|
if (!raw) return [];
|
|
3325
3391
|
let list;
|
|
@@ -3329,7 +3395,7 @@ function readYesLockSeal(raw, asJson) {
|
|
|
3329
3395
|
exitMissionError('--yes-lock takes a JSON list of {step, text, categories}', 2, asJson);
|
|
3330
3396
|
}
|
|
3331
3397
|
const ok = Array.isArray(list) && list.every(l => l && Number.isInteger(l.step) && l.step > 0 && Array.isArray(l.categories) && l.categories.length && l.categories.every(c => YES_LOCK_CATEGORIES.has(c)));
|
|
3332
|
-
if (!ok) exitMissionError('--yes-lock takes a JSON list of {step, text, categories}, with categories from publish, tag, post, send, merge, deploy', 2, asJson);
|
|
3398
|
+
if (!ok) exitMissionError('--yes-lock takes a JSON list of {step, text, categories}, with categories from publish, tag, post, send, merge, deploy, spend', 2, asJson);
|
|
3333
3399
|
return list.map(l => ({ step: l.step, text: String(l.text || '').slice(0, 200), categories: [...new Set(l.categories)] }));
|
|
3334
3400
|
}
|
|
3335
3401
|
|
|
@@ -3797,10 +3863,6 @@ function chooseNextMissionAnalysis(mission, root = process.cwd()) {
|
|
|
3797
3863
|
return { target: null, near_misses: [] };
|
|
3798
3864
|
}
|
|
3799
3865
|
|
|
3800
|
-
function chooseNextMissionTarget(mission, root = process.cwd()) {
|
|
3801
|
-
return chooseNextMissionAnalysis(mission, root).target;
|
|
3802
|
-
}
|
|
3803
|
-
|
|
3804
3866
|
function normalizeMissionOwner(value) {
|
|
3805
3867
|
return String(value || '').trim().toLowerCase();
|
|
3806
3868
|
}
|
|
@@ -4512,6 +4574,15 @@ function normalizedObjective(text) {
|
|
|
4512
4574
|
return String(text || '').toLowerCase().replace(/\s+/g, ' ').trim();
|
|
4513
4575
|
}
|
|
4514
4576
|
|
|
4577
|
+
// True when `existing` runs under the lock `wanted` carries: the start
|
|
4578
|
+
// carries none, or the same seal.
|
|
4579
|
+
function lockCovered(existing, wanted) {
|
|
4580
|
+
const want = (wanted && wanted.yes_locks && wanted.yes_locks.steps) || [];
|
|
4581
|
+
if (!want.length) return true;
|
|
4582
|
+
const have = (existing && existing.yes_locks && existing.yes_locks.steps) || [];
|
|
4583
|
+
return JSON.stringify(have) === JSON.stringify(want);
|
|
4584
|
+
}
|
|
4585
|
+
|
|
4515
4586
|
function findActiveTwinMission(objective, owner, root = process.cwd()) {
|
|
4516
4587
|
const wantObjective = normalizedObjective(objective);
|
|
4517
4588
|
const wantOwner = String(owner || '').trim().toLowerCase();
|
|
@@ -4547,6 +4618,11 @@ function startMission(args, options = {}) {
|
|
|
4547
4618
|
const idLikeObjective = String(mission.objective || '').trim();
|
|
4548
4619
|
if (/^mission-\d{4}-\d{2}-\d{2}-/.test(idLikeObjective)) {
|
|
4549
4620
|
const existing = resolveMission(idLikeObjective);
|
|
4621
|
+
if (existing && !lockCovered(existing, mission)) {
|
|
4622
|
+
// A start carrying a yes-lock never resumes a job under a weaker lock:
|
|
4623
|
+
// it would run without it. (Codex review round 4 on #1483.)
|
|
4624
|
+
exitMissionError(`${existing.id} is not under the yes-lock this start carries, so resuming it would run without the yes-lock; start a new job instead`, 2, asJson);
|
|
4625
|
+
}
|
|
4550
4626
|
if (existing) {
|
|
4551
4627
|
printJsonOrText(
|
|
4552
4628
|
{ ok: true, action: 'mission_recovered', recovered: true, mission: existing, note: 'objective looked like a mission id; recovered the existing mission instead of creating a new one' },
|
|
@@ -4560,55 +4636,57 @@ function startMission(args, options = {}) {
|
|
|
4560
4636
|
}
|
|
4561
4637
|
exitMissingMission(idLikeObjective, 1, asJson);
|
|
4562
4638
|
}
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
action: 'mission_destination_change_proposed',
|
|
4585
|
-
reused: true,
|
|
4586
|
-
operator_gate_required: true,
|
|
4587
|
-
mission: saved,
|
|
4588
|
-
proposal,
|
|
4589
|
-
},
|
|
4590
|
-
[
|
|
4591
|
-
`Destination change proposed for ${saved.id}.`,
|
|
4592
|
-
`Current destination: ${saved.drive.destination}`,
|
|
4593
|
-
`Proposed destination: ${proposal.destination}`,
|
|
4594
|
-
`Operator gate: ${proposal.reason}.`,
|
|
4595
|
-
],
|
|
4596
|
-
asJson,
|
|
4597
|
-
);
|
|
4598
|
-
return;
|
|
4599
|
-
}
|
|
4639
|
+
// A start that carries a yes-lock never reuses a twin under a different
|
|
4640
|
+
// lock (or none): the reused job would run without it. It gets a fresh
|
|
4641
|
+
// job under the lock instead. (Codex review rounds 3 and 4 on #1483.)
|
|
4642
|
+
const twinCandidate = hasFlag(args, '--duplicate') ? null : findActiveTwinMission(mission.objective, mission.owner);
|
|
4643
|
+
if (twinCandidate && lockCovered(twinCandidate, mission)) {
|
|
4644
|
+
const twin = twinCandidate;
|
|
4645
|
+
const requestedDestination = mission.drive?.destination || '';
|
|
4646
|
+
if (requestedDestination && twin.drive?.destination && requestedDestination !== twin.drive.destination) {
|
|
4647
|
+
const proposed = proposeMissionDestinationChange(twin, requestedDestination);
|
|
4648
|
+
const proposal = proposed.drive.pending_destination_proposal;
|
|
4649
|
+
const missionRoot = twin.worktree_root || process.cwd();
|
|
4650
|
+
const { mission: saved } = saveMission(
|
|
4651
|
+
proposed,
|
|
4652
|
+
missionRoot,
|
|
4653
|
+
'mission_destination_change_proposed',
|
|
4654
|
+
{
|
|
4655
|
+
current_destination: twin.drive.destination,
|
|
4656
|
+
proposed_destination: proposal.destination,
|
|
4657
|
+
reason: proposal.reason,
|
|
4658
|
+
},
|
|
4659
|
+
);
|
|
4600
4660
|
printJsonOrText(
|
|
4601
|
-
{
|
|
4661
|
+
{
|
|
4662
|
+
ok: true,
|
|
4663
|
+
action: 'mission_destination_change_proposed',
|
|
4664
|
+
reused: true,
|
|
4665
|
+
operator_gate_required: true,
|
|
4666
|
+
mission: saved,
|
|
4667
|
+
proposal,
|
|
4668
|
+
},
|
|
4602
4669
|
[
|
|
4603
|
-
`
|
|
4604
|
-
`
|
|
4605
|
-
`
|
|
4606
|
-
`
|
|
4670
|
+
`Destination change proposed for ${saved.id}.`,
|
|
4671
|
+
`Current destination: ${saved.drive.destination}`,
|
|
4672
|
+
`Proposed destination: ${proposal.destination}`,
|
|
4673
|
+
`Operator gate: ${proposal.reason}.`,
|
|
4607
4674
|
],
|
|
4608
4675
|
asJson,
|
|
4609
4676
|
);
|
|
4610
4677
|
return;
|
|
4611
4678
|
}
|
|
4679
|
+
printJsonOrText(
|
|
4680
|
+
{ ok: true, action: 'mission_reused', reused: true, mission: twin, note: 'an active mission with this objective and owner already exists; resumed instead of cloning (pass --duplicate to force a second one)' },
|
|
4681
|
+
[
|
|
4682
|
+
`Already active: ${twin.id} (${twin.status})`,
|
|
4683
|
+
`Same objective, same owner, reusing it instead of starting a clone.`,
|
|
4684
|
+
`Resume: atris mission run ${twin.id}`,
|
|
4685
|
+
`Really want a second one: re-run with --duplicate`,
|
|
4686
|
+
],
|
|
4687
|
+
asJson,
|
|
4688
|
+
);
|
|
4689
|
+
return;
|
|
4612
4690
|
}
|
|
4613
4691
|
// Intake gate: a mission without a verifier is a planning wish, and planning
|
|
4614
4692
|
// wishes silt up the queue (11 swept on 2026-07-06 with zero ticks). Refuse
|
|
@@ -4736,6 +4814,13 @@ function startMission(args, options = {}) {
|
|
|
4736
4814
|
lane: saved.lane,
|
|
4737
4815
|
verifier: saved.verifier,
|
|
4738
4816
|
});
|
|
4817
|
+
if (saved.yes_lock_parent && saved.yes_locks && saved.yes_locks.steps) {
|
|
4818
|
+
// the lock record names the job it came from, before any command is held
|
|
4819
|
+
try {
|
|
4820
|
+
require('../lib/yes-lock').writeYesLocks(path.join(require('../lib/state-home').atrisHome(), 'state'), saved.id,
|
|
4821
|
+
{ seal: saved.yes_locks.steps, parentMissionId: saved.yes_lock_parent });
|
|
4822
|
+
} catch {}
|
|
4823
|
+
}
|
|
4739
4824
|
const worktreeBaseline = captureMissionWorktreeBaseline(saved, process.cwd(), codeBaseline);
|
|
4740
4825
|
const nextTickCommand = `atris mission tick ${saved.id}${saved.verifier ? ' --verify' : ''}`;
|
|
4741
4826
|
const payload = {
|
|
@@ -12807,6 +12892,9 @@ module.exports = {
|
|
|
12807
12892
|
usefulClaudeReceiptSummary,
|
|
12808
12893
|
autoQueueMemberNextPush,
|
|
12809
12894
|
recordMemberHandoff,
|
|
12895
|
+
updateMemberHandoff,
|
|
12896
|
+
resolveMemberWorkspaceRoot,
|
|
12897
|
+
isYesLockWord,
|
|
12810
12898
|
closeMemberHandoffsForReceiver,
|
|
12811
12899
|
cappedClaudeReceiptText,
|
|
12812
12900
|
extractLayerFromReceiptText,
|
package/commands/now.js
CHANGED
|
@@ -273,13 +273,6 @@ function taskReceiptTitle(row) {
|
|
|
273
273
|
return compactLine(row?.state?.title || row?.title || row?.task_id || 'untitled work');
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
-
// The receipts behind "Completed receipts today", as lines a human can read:
|
|
277
|
-
// one per receipt, landed result + human proof state. Shares the collector
|
|
278
|
-
// with the count so the number and the visible list always match.
|
|
279
|
-
function todayTaskReceiptLines(root = process.cwd(), date = new Date(), limit = 6) {
|
|
280
|
-
return formatTaskReceiptLines(collectTaskReceiptsToday(root, date), limit);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
276
|
// Format an already-collected receipt list, so a caller that needs both the
|
|
284
277
|
// count and the lines (renderDefaultNow) parses the receipt files only once.
|
|
285
278
|
function formatTaskReceiptLines(receipts, limit = 6) {
|
package/commands/plugin.js
CHANGED
|
@@ -2,7 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const { execSync } = require('child_process');
|
|
5
|
-
const { findAllSkills
|
|
5
|
+
const { findAllSkills } = require('./skill');
|
|
6
6
|
|
|
7
7
|
// --- Recursive Copy Helper ---
|
|
8
8
|
|
package/commands/pull.js
CHANGED
|
@@ -7,7 +7,7 @@ const { loadConfig } = require('../utils/config');
|
|
|
7
7
|
const { getLogPath } = require('../lib/file-ops');
|
|
8
8
|
const { parseJournalSections, mergeSections, reconstructJournal } = require('../lib/journal');
|
|
9
9
|
const { loadBusinesses, businessMatchesSlug } = require('./business');
|
|
10
|
-
const { loadManifest, saveManifest,
|
|
10
|
+
const { loadManifest, saveManifest, buildManifest, computeLocalHashes, threeWayCompare, isIgnoredSyncPath } = require('../lib/manifest');
|
|
11
11
|
const { normalizeWikiOnlyPrefix } = require('../lib/wiki');
|
|
12
12
|
const { emitSyncEvent, startTimer } = require('../lib/sync-telemetry');
|
|
13
13
|
const { resolveSafeOutputDir } = require('../lib/workspace-safety');
|
package/commands/push.js
CHANGED
package/commands/radar.js
CHANGED
|
@@ -685,10 +685,6 @@ function taskBinding(source, scope = null) {
|
|
|
685
685
|
return { task_source: source, task_scope: scope };
|
|
686
686
|
}
|
|
687
687
|
|
|
688
|
-
function taskBindingForProjection(task) {
|
|
689
|
-
return task ? taskBinding('repo_task_projection', 'repo') : taskBinding(null, null);
|
|
690
|
-
}
|
|
691
|
-
|
|
692
688
|
function taskSourceLabel(sources = []) {
|
|
693
689
|
if (sources.includes('member_loop_lock')) return 'member loop lock';
|
|
694
690
|
if (sources.includes('mission_lock')) return 'mission lock';
|
package/commands/recap.js
CHANGED
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
taskCommand,
|
|
14
14
|
} = require('../lib/first-minute');
|
|
15
15
|
const { isRealTestRunnerProof, quoteVerifierCommand } = require('../lib/verifier-quality');
|
|
16
|
+
const { buildAgentToday, renderAgentToday, hasActivity, ranAgent } = require('../lib/agent-today');
|
|
16
17
|
|
|
17
18
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
18
19
|
const DEFAULT_DAYS = 7;
|
|
@@ -38,7 +39,8 @@ function readProjection(root) {
|
|
|
38
39
|
|
|
39
40
|
function loadTasks(root) {
|
|
40
41
|
const taskDb = loadTaskDb();
|
|
41
|
-
|
|
42
|
+
// Reading never creates the db: a first run before any setup writes nothing.
|
|
43
|
+
if (taskDb && fs.existsSync(taskDb.getDbPath())) {
|
|
42
44
|
try {
|
|
43
45
|
const db = taskDb.open();
|
|
44
46
|
const ws = taskDb.workspaceRoot(root);
|
|
@@ -335,11 +337,14 @@ function printRecapHelp() {
|
|
|
335
337
|
atris recap - what got done, in spoken lines
|
|
336
338
|
|
|
337
339
|
atris recap a few spoken lines: waiting for your ok, still being checked
|
|
340
|
+
atris recap today what your ai coding agent did in this folder today
|
|
338
341
|
atris recap --verbose the old report
|
|
339
342
|
atris recap --days 30 widen the window
|
|
340
343
|
atris recap --share paste-ready summary for Slack, email, or a customer
|
|
341
344
|
atris recap --json structured output for agents and dashboards
|
|
342
345
|
|
|
346
|
+
in a folder with no atris setup, recap shows today by default.
|
|
347
|
+
today reads claude code and codex logs on this computer plus git. it sends nothing.
|
|
343
348
|
certified work names atris task accept. uncertified stays still being checked.
|
|
344
349
|
use --verbose or --share for the old report.
|
|
345
350
|
`);
|
|
@@ -351,6 +356,11 @@ function recapAtris(args = []) {
|
|
|
351
356
|
return;
|
|
352
357
|
}
|
|
353
358
|
const cwd = process.cwd();
|
|
359
|
+
if (args[0] === 'today') {
|
|
360
|
+
const today = buildAgentToday({ cwd });
|
|
361
|
+
console.log(args.includes('--json') ? JSON.stringify(today, null, 2) : renderAgentToday(today));
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
354
364
|
const taskDb = loadTaskDb();
|
|
355
365
|
let root = cwd;
|
|
356
366
|
try {
|
|
@@ -370,6 +380,20 @@ function recapAtris(args = []) {
|
|
|
370
380
|
const daysIdx = args.indexOf('--days');
|
|
371
381
|
const days = daysIdx !== -1 ? Number(args[daysIdx + 1]) : DEFAULT_DAYS;
|
|
372
382
|
const data = buildRecapData(root, { days });
|
|
383
|
+
// Before any setup, or when the board has nothing to say, the useful
|
|
384
|
+
// first minute is what your ai agent already did in this folder today.
|
|
385
|
+
const boardQuiet = data.empty
|
|
386
|
+
|| ['shipped', 'waiting', 'checking', 'inProgress'].every((k) => !(data[k] || []).length);
|
|
387
|
+
if (boardQuiet && !args.includes('--verbose') && !args.includes('--full') && !args.includes('--share')) {
|
|
388
|
+
const today = buildAgentToday({ cwd });
|
|
389
|
+
// A brand new folder already names its last commit in the first
|
|
390
|
+
// minute, so commits alone keep that; an ai session here wins.
|
|
391
|
+
const fresh = data.empty && isFreshWorkspace(root) && !inProjectSubdir;
|
|
392
|
+
if (hasActivity(today) && (ranAgent(today) || !fresh)) {
|
|
393
|
+
console.log(args.includes('--json') ? JSON.stringify(today, null, 2) : renderAgentToday(today));
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
373
397
|
if (
|
|
374
398
|
data.empty
|
|
375
399
|
&& isFreshWorkspace(root)
|
package/commands/report.js
CHANGED
|
@@ -110,10 +110,6 @@ function buildDoneTasks(root, cutoff) {
|
|
|
110
110
|
});
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
function buildLandings(root, cutoff) {
|
|
114
|
-
return buildDoneTasks(root, cutoff).filter(task => task.landed);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
113
|
function landingStatusText(task) {
|
|
118
114
|
const status = String(task?.landing_status || 'unchecked').replace(/_/g, ' ');
|
|
119
115
|
const reason = String(task?.landing_reason || '').replace(/_/g, ' ');
|
package/commands/review.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const path = require('path');
|
|
12
|
-
const {
|
|
12
|
+
const { spawnSync } = require('child_process');
|
|
13
13
|
|
|
14
14
|
function findReviewEngine() {
|
|
15
15
|
// Look for review_engine.py in common locations
|
package/commands/setup.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
1
|
const { loadCredentials, promptUser } = require('../utils/auth');
|
|
4
2
|
const { apiRequestJson } = require('../utils/api');
|
|
5
3
|
|
|
@@ -73,6 +71,12 @@ async function setupAtris() {
|
|
|
73
71
|
token: creds.token,
|
|
74
72
|
});
|
|
75
73
|
|
|
74
|
+
if (!result.ok && result.status === 401) {
|
|
75
|
+
console.error('\n Your saved login has expired, so Atris could not list your businesses.');
|
|
76
|
+
console.error(' Run "atris login", then "atris setup" again.');
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
76
80
|
if (!result.ok) {
|
|
77
81
|
console.error(`\n Could not fetch businesses: ${result.error || 'Unknown error'}`);
|
|
78
82
|
console.error(' You can add one later with: atris business add <slug>');
|
package/commands/slop.js
CHANGED
|
@@ -418,9 +418,15 @@ function findDeadCode(root = process.cwd(), opts = {}) {
|
|
|
418
418
|
const candidates = scanDirs.flatMap((d) => listJsFiles(d));
|
|
419
419
|
if (!candidates.length) return { root, candidates: 0, dead: [], testOnly: [] };
|
|
420
420
|
|
|
421
|
-
// 1) reachability BFS from entrypoints over static edges
|
|
421
|
+
// 1) reachability BFS from entrypoints over static edges. A file that runs
|
|
422
|
+
// itself when started directly (`node lib/x.js`, guarded by
|
|
423
|
+
// `require.main === module`) is an entrypoint too, even if only a test
|
|
424
|
+
// imports it.
|
|
422
425
|
const reached = new Set();
|
|
423
|
-
const
|
|
426
|
+
const runsItself = (f) => {
|
|
427
|
+
try { return /\brequire\.main\s*===\s*module\b/.test(fs.readFileSync(f, 'utf8')); } catch { return false; }
|
|
428
|
+
};
|
|
429
|
+
const queue = [...deadEntrypoints(root), ...candidates.filter(runsItself)];
|
|
424
430
|
while (queue.length) {
|
|
425
431
|
const f = queue.pop();
|
|
426
432
|
if (reached.has(f)) continue;
|