atris 3.35.0 → 3.37.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 +37 -0
- 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 +22 -4
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +2 -0
- package/ax +147 -19
- package/bin/atris.js +576 -267
- package/commands/activate.js +194 -88
- package/commands/agents.js +166 -0
- package/commands/autoland.js +459 -107
- 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/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- 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/engine.js +993 -32
- package/commands/experiments.js +28 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/gm.js +23 -0
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +72 -44
- package/commands/interview.js +67 -1
- package/commands/land.js +152 -52
- package/commands/lifecycle.js +39 -3
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +511 -34
- package/commands/mission.js +3144 -338
- package/commands/next.js +137 -0
- package/commands/now.js +220 -25
- 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/rainmaker.js +49 -0
- package/commands/report.js +415 -0
- package/commands/scout.js +147 -0
- package/commands/search.js +363 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/stream.js +861 -0
- package/commands/study.js +693 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +1346 -117
- package/commands/team.js +73 -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 +234 -13
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +331 -34
- package/lib/autoland.js +319 -54
- package/lib/ax-auto-lane.js +96 -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 +2219 -67
- 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/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/next-moves.js +327 -10
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -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/router-brain.js +352 -0
- package/lib/runner-command.js +10 -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 +108 -29
- package/lib/task-proof.js +23 -1
- 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/lib/task-db.js
CHANGED
|
@@ -240,22 +240,43 @@ function newId() {
|
|
|
240
240
|
return head + tail;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
243
|
+
// Resolve the canonical workspace root so `atris task` from any subdirectory
|
|
244
|
+
// keys the SAME store as the repo root. Precedence (nearest ancestor wins per
|
|
245
|
+
// pass):
|
|
246
|
+
// 1. A deliberate workspace spine — .atris/business.json (a bound customer
|
|
247
|
+
// sub-workspace) or atris/atris.md (the framework protocol file). These are
|
|
248
|
+
// intentional nested workspaces INSIDE a repo and must stay isolated.
|
|
249
|
+
// 2. The git toplevel (.git). One git repo == one workspace; this rescues a
|
|
250
|
+
// subdirectory whose only markers are a bare atris/ or .atris/ dir.
|
|
251
|
+
// 3. Legacy fallback for non-git trees: a bare atris/ or .atris/ dir.
|
|
252
|
+
//
|
|
253
|
+
// Before this, a bare atris/ or .atris/ dir short-circuited ahead of the git
|
|
254
|
+
// root, so a subdir like backend/ (which has an atris/ folder plus a stray
|
|
255
|
+
// .atris) hijacked itself as a second workspace root and split task/usage state
|
|
256
|
+
// into backend/.atris (proven footgun 2026-07-16). Checking .git only at the
|
|
257
|
+
// same level meant the walk returned before ever reaching the real repo root.
|
|
247
258
|
function findWorkspaceRoot(start) {
|
|
248
|
-
|
|
249
|
-
// Cap
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
+
const origin = path.resolve(start || process.cwd());
|
|
260
|
+
// Cap each walk at 32 levels to avoid pathological symlink loops.
|
|
261
|
+
const walkUp = (test) => {
|
|
262
|
+
let cur = origin;
|
|
263
|
+
for (let i = 0; i < 32; i++) {
|
|
264
|
+
if (test(cur)) return cur;
|
|
265
|
+
const parent = path.dirname(cur);
|
|
266
|
+
if (parent === cur) break;
|
|
267
|
+
cur = parent;
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
};
|
|
271
|
+
const spine = walkUp((dir) => fs.existsSync(path.join(dir, '.atris', 'business.json'))
|
|
272
|
+
|| fs.existsSync(path.join(dir, 'atris', 'atris.md')));
|
|
273
|
+
if (spine) return spine;
|
|
274
|
+
const gitRoot = walkUp((dir) => fs.existsSync(path.join(dir, '.git')));
|
|
275
|
+
if (gitRoot) return gitRoot;
|
|
276
|
+
const legacy = walkUp((dir) => fs.existsSync(path.join(dir, 'atris'))
|
|
277
|
+
|| fs.existsSync(path.join(dir, '.atris')));
|
|
278
|
+
if (legacy) return legacy;
|
|
279
|
+
return origin;
|
|
259
280
|
}
|
|
260
281
|
|
|
261
282
|
function workspaceRoot(cwd) {
|
|
@@ -558,17 +579,18 @@ function doneTask(db, { id, status, actor, allowReview = false, action, proof, a
|
|
|
558
579
|
// (e.g. the 109 duplicate "Loop tick:" orphans fail-closed before 'archived'
|
|
559
580
|
// existed — cluster 1 of failed-tasks-analysis-2026-07-03). It permits the
|
|
560
581
|
// failed→archived transition and records the prior status in
|
|
561
|
-
// metadata.archived_from.
|
|
562
|
-
//
|
|
563
|
-
|
|
582
|
+
// metadata.archived_from. Individual archive commands never archive 'done'
|
|
583
|
+
// rows. The clear-done sweep opts in through fromDone after selecting only the
|
|
584
|
+
// same 'done' rows counted by `atris status`.
|
|
585
|
+
function archiveTask(db, { id, actor, reason, fromFailed = false, fromDone = false } = {}) {
|
|
564
586
|
if (!id) throw new Error('id required');
|
|
565
587
|
const reasonText = String(reason || '').trim();
|
|
566
588
|
if (!reasonText) throw new Error('reason required');
|
|
567
589
|
const row = getTask(db, id);
|
|
568
590
|
if (!row) return { archived: false, reason: 'not_found' };
|
|
569
|
-
const allowedStatuses =
|
|
570
|
-
|
|
571
|
-
|
|
591
|
+
const allowedStatuses = ['open', 'claimed', 'review'];
|
|
592
|
+
if (fromFailed) allowedStatuses.push('failed');
|
|
593
|
+
if (fromDone) allowedStatuses.push('done');
|
|
572
594
|
if (!allowedStatuses.includes(row.status)) {
|
|
573
595
|
return { archived: false, reason: `already_${row.status}` };
|
|
574
596
|
}
|
|
@@ -578,7 +600,8 @@ function archiveTask(db, { id, actor, reason, fromFailed = false } = {}) {
|
|
|
578
600
|
metadata.archived_at = new Date(now).toISOString();
|
|
579
601
|
metadata.archived_by = actor || process.env.ATRIS_AGENT_ID || process.env.USER || null;
|
|
580
602
|
metadata.approval_status = 'archived';
|
|
581
|
-
if (row.status === 'failed') metadata.archived_from =
|
|
603
|
+
if (row.status === 'failed' || row.status === 'done') metadata.archived_from = row.status;
|
|
604
|
+
const terminalAt = row.status === 'done' && row.done_at ? row.done_at : now;
|
|
582
605
|
const statusPlaceholders = allowedStatuses.map(() => '?').join(', ');
|
|
583
606
|
const result = withBusyRetry(() => db.prepare(`
|
|
584
607
|
UPDATE tasks
|
|
@@ -588,7 +611,7 @@ function archiveTask(db, { id, actor, reason, fromFailed = false } = {}) {
|
|
|
588
611
|
metadata = ?
|
|
589
612
|
WHERE id = ?
|
|
590
613
|
AND status IN (${statusPlaceholders})
|
|
591
|
-
`).run(
|
|
614
|
+
`).run(terminalAt, now, JSON.stringify(metadata), id, ...allowedStatuses));
|
|
592
615
|
if (result.changes !== 1) return { archived: false, reason: 'stale_task_state' };
|
|
593
616
|
const updated = getTask(db, id);
|
|
594
617
|
const event = appendTaskEvent(db, {
|
|
@@ -596,7 +619,10 @@ function archiveTask(db, { id, actor, reason, fromFailed = false } = {}) {
|
|
|
596
619
|
workspaceRoot: updated.workspace_root,
|
|
597
620
|
actor: metadata.archived_by,
|
|
598
621
|
eventType: 'archived',
|
|
599
|
-
payload: {
|
|
622
|
+
payload: {
|
|
623
|
+
reason: reasonText,
|
|
624
|
+
...(['failed', 'done'].includes(row.status) ? { previous_status: row.status } : {}),
|
|
625
|
+
},
|
|
600
626
|
});
|
|
601
627
|
const logs = appendTaskCompletionLogs(db, updated, {
|
|
602
628
|
status: 'archived',
|
|
@@ -684,7 +710,7 @@ function recordReviewPassActor(metadata, claimedBy, rawActor) {
|
|
|
684
710
|
return { builder: builder || null, independent };
|
|
685
711
|
}
|
|
686
712
|
|
|
687
|
-
function readyTask(db, { id, actor, proof, lesson, nextTask, landing = {}, resultTrace }) {
|
|
713
|
+
function readyTask(db, { id, actor, proof, lesson, nextTask, landing = {}, resultTrace, result: resultSentence, reason }) {
|
|
688
714
|
if (!id) throw new Error('id required');
|
|
689
715
|
const text = String(proof || '').trim();
|
|
690
716
|
if (!text) throw new Error('proof required');
|
|
@@ -703,6 +729,10 @@ function readyTask(db, { id, actor, proof, lesson, nextTask, landing = {}, resul
|
|
|
703
729
|
metadata.latest_agent_proof = text;
|
|
704
730
|
metadata.latest_agent_lesson = String(lesson || '').trim() || null;
|
|
705
731
|
metadata.latest_agent_next_task = String(nextTask || '').trim() || null;
|
|
732
|
+
const resultText = cleanLandingText(resultSentence);
|
|
733
|
+
if (resultText) metadata.result = resultText;
|
|
734
|
+
const reasonText = cleanLandingText(reason);
|
|
735
|
+
if (reasonText) metadata.result_reason = reasonText;
|
|
706
736
|
const landingInput = landing && typeof landing === 'object' ? landing : {};
|
|
707
737
|
for (const key of ['happened', 'checked', 'tested', 'decision']) {
|
|
708
738
|
const cleaned = cleanLandingText(landingInput[key]);
|
|
@@ -735,6 +765,7 @@ function readyTask(db, { id, actor, proof, lesson, nextTask, landing = {}, resul
|
|
|
735
765
|
review_pass_count: reviewPassCount,
|
|
736
766
|
agent_certified: metadata.agent_certified === true,
|
|
737
767
|
agent_certification_policy: metadata.agent_certification_policy || null,
|
|
768
|
+
result: metadata.result || null,
|
|
738
769
|
};
|
|
739
770
|
if (resultTrace && typeof resultTrace === 'object') {
|
|
740
771
|
payload.result_trace = resultTrace;
|
|
@@ -756,13 +787,46 @@ function readyTask(db, { id, actor, proof, lesson, nextTask, landing = {}, resul
|
|
|
756
787
|
return { ready: true, event, row: updated };
|
|
757
788
|
}
|
|
758
789
|
|
|
759
|
-
function
|
|
790
|
+
function setTaskResult(db, { id, actor, result }) {
|
|
791
|
+
if (!id) throw new Error('id required');
|
|
792
|
+
const text = cleanLandingText(result);
|
|
793
|
+
if (!text) throw new Error('result required');
|
|
794
|
+
const row = getTask(db, id);
|
|
795
|
+
if (!row) return { saved: false, reason: 'not_found' };
|
|
796
|
+
const now = Date.now();
|
|
797
|
+
const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
|
|
798
|
+
metadata.result = text;
|
|
799
|
+
metadata.result_at = new Date(now).toISOString();
|
|
800
|
+
metadata.result_by = actor || process.env.ATRIS_AGENT_ID || process.env.USER || null;
|
|
801
|
+
const updated = withBusyRetry(() => db.prepare(`
|
|
802
|
+
UPDATE tasks
|
|
803
|
+
SET metadata = ?,
|
|
804
|
+
updated_at = ?
|
|
805
|
+
WHERE id = ?
|
|
806
|
+
`).run(JSON.stringify(metadata), now, id));
|
|
807
|
+
if (updated.changes !== 1) return { saved: false, reason: 'stale_task_state' };
|
|
808
|
+
const latest = getTask(db, id);
|
|
809
|
+
const event = appendTaskEvent(db, {
|
|
810
|
+
taskId: id,
|
|
811
|
+
workspaceRoot: latest.workspace_root,
|
|
812
|
+
actor: metadata.result_by,
|
|
813
|
+
eventType: 'result_set',
|
|
814
|
+
payload: { result: text },
|
|
815
|
+
});
|
|
816
|
+
return { saved: true, event, row: latest };
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function reviseTask(db, { id, actor, note, allowDone = false }) {
|
|
760
820
|
if (!id) throw new Error('id required');
|
|
761
821
|
const text = String(note || '').trim();
|
|
762
822
|
if (!text) throw new Error('note required');
|
|
763
823
|
const row = getTask(db, id);
|
|
764
824
|
if (!row) return { revised: false, reason: 'not_found' };
|
|
765
|
-
|
|
825
|
+
const acceptedDone = allowDone
|
|
826
|
+
&& row.status === 'done'
|
|
827
|
+
&& row.metadata
|
|
828
|
+
&& row.metadata.approval_status === 'accepted';
|
|
829
|
+
if (row.status !== 'review' && !acceptedDone) {
|
|
766
830
|
return { revised: false, reason: `not_reviewable_${row.status}` };
|
|
767
831
|
}
|
|
768
832
|
const now = Date.now();
|
|
@@ -787,7 +851,19 @@ function reviseTask(db, { id, actor, note }) {
|
|
|
787
851
|
metadata.human_revision_at = new Date(now).toISOString();
|
|
788
852
|
metadata.human_revision_by = actor || process.env.ATRIS_AGENT_ID || process.env.USER || null;
|
|
789
853
|
metadata.human_revision_note = text;
|
|
854
|
+
if (acceptedDone) {
|
|
855
|
+
for (const key of [
|
|
856
|
+
'accepted_at',
|
|
857
|
+
'accepted_by',
|
|
858
|
+
'auto_accepted_at',
|
|
859
|
+
'auto_accepted_by',
|
|
860
|
+
'auto_accept_policy',
|
|
861
|
+
]) {
|
|
862
|
+
delete metadata[key];
|
|
863
|
+
}
|
|
864
|
+
}
|
|
790
865
|
const revisedStatus = row.claimed_by ? 'claimed' : 'open';
|
|
866
|
+
const expectedStatus = acceptedDone ? 'done' : 'review';
|
|
791
867
|
const result = withBusyRetry(() => db.prepare(`
|
|
792
868
|
UPDATE tasks
|
|
793
869
|
SET status = ?,
|
|
@@ -795,8 +871,8 @@ function reviseTask(db, { id, actor, note }) {
|
|
|
795
871
|
updated_at = ?,
|
|
796
872
|
metadata = ?
|
|
797
873
|
WHERE id = ?
|
|
798
|
-
AND status =
|
|
799
|
-
`).run(revisedStatus, now, JSON.stringify(metadata), id));
|
|
874
|
+
AND status = ?
|
|
875
|
+
`).run(revisedStatus, now, JSON.stringify(metadata), id, expectedStatus));
|
|
800
876
|
if (result.changes !== 1) return { revised: false, reason: 'not_updated' };
|
|
801
877
|
const updated = getTask(db, id);
|
|
802
878
|
const event = appendTaskEvent(db, {
|
|
@@ -808,6 +884,7 @@ function reviseTask(db, { id, actor, note }) {
|
|
|
808
884
|
note: text,
|
|
809
885
|
approval_status: 'revise',
|
|
810
886
|
revision_count: revisionCount,
|
|
887
|
+
...(acceptedDone ? { previous_status: row.status } : {}),
|
|
811
888
|
},
|
|
812
889
|
});
|
|
813
890
|
const episode = taskEpisodeFromRevision(updated, event, event.payload);
|
|
@@ -1846,6 +1923,7 @@ function taskProjection(db, {
|
|
|
1846
1923
|
id: row.id,
|
|
1847
1924
|
...(refById.get(row.id) || {}),
|
|
1848
1925
|
title: row.title,
|
|
1926
|
+
result: row.metadata && row.metadata.result || null,
|
|
1849
1927
|
status: row.status,
|
|
1850
1928
|
tag: row.tag,
|
|
1851
1929
|
workspace_root: row.workspace_root,
|
|
@@ -1975,6 +2053,7 @@ module.exports = {
|
|
|
1975
2053
|
relabelArchivedTasks,
|
|
1976
2054
|
isJune10BacklogResetMarker,
|
|
1977
2055
|
readyTask,
|
|
2056
|
+
setTaskResult,
|
|
1978
2057
|
reviseTask,
|
|
1979
2058
|
stageTask,
|
|
1980
2059
|
noteTask,
|
package/lib/task-proof.js
CHANGED
|
@@ -7,6 +7,11 @@ const FILE_PROOF_RE = /(?:^|[\s'"`])(?:\.{0,2}\/|~\/|\/Users\/|src\/|scripts\/|a
|
|
|
7
7
|
const PATH_ONLY_PROOF_RE = /(?:^|[\s'"`])(?:\.{0,2}\/|~\/|\/Users\/|\/private\/|\/var\/|atris\/runs\/|\.atris\/state\/)[^\s'"`,;)]+(?:[.](?:json|jsonl|md|log|txt|png|jpg|jpeg|pdf))?/i;
|
|
8
8
|
const RECEIPT_OR_ARTIFACT_RE = /\b(?:receipt|artifact|screenshot|log|trace|path=|file=|bytes=|model=|opened=|https?:\/\/)\b/i;
|
|
9
9
|
const RESULT_PAIR_RE = /\b(?:typecheck|build|smoke|test|pytest|verifier|validation|validated|verified|render|diff|sync|lineage|projection)\b.{0,80}\b(?:pass|passed|failed|green|ok|exit\s*0|reviewed)\b|\b(?:pass|passed|failed|green|ok|exit\s*0|reviewed)\b.{0,80}\b(?:typecheck|build|smoke|test|pytest|verifier|validation|validated|verified|render|diff|sync|lineage|projection)\b/i;
|
|
10
|
+
const SUITE_GREEN_CLAIM_RE = /\b(?:tests|test\s+suite|suite)\s+(?:is|are|was|were)?\s*(?:all\s+)?(?:green|pass(?:es|ed)?|ok)\b|\b(?:all|full|whole|entire)\s+(?:tests?|test\s+suite|suite)\s+(?:is|are|was|were)?\s*(?:green|pass(?:es|ed)?|ok)\b|\ball\s+green\b|\b(?:npm|pnpm|yarn)\s+(?:run\s+)?test\b\x60?(?:\s+(?:\d+\s*\/\s*\d+|all|suite))?\s+(?:is\s+)?(?:green|pass(?:es|ed)?|ok)\b/i;
|
|
11
|
+
const CI_RUN_CITATION_RE = /https?:\/\/github[.]com\/[^/\s]+\/[^/\s]+\/actions\/runs\/\d+\b|\brun_id\s*=\s*\d+\b|\brun(?:\s+id)?\s*(?:=|:)?\s*\d+\b/i;
|
|
12
|
+
const COMMIT_PINNED_LOCAL_VERIFY_RE = /\bcommit\s+[0-9a-f]{7,40}\b/i;
|
|
13
|
+
const EXIT_ZERO_RE = /\bexit(?:ed)?(?:\s+code)?\s*0\b/i;
|
|
14
|
+
const SUITE_GREEN_CITATION_REASON = 'cite the CI run URL, run id, or commit-pinned verify command with exit 0 before claiming the suite is green';
|
|
10
15
|
const HUMAN_PROOF_RE = /\b(?:team human approved|human approved|human approval|approved by|accepted by|reviewed by|customer replied|customer approved|customer accepted|replied)\b/i;
|
|
11
16
|
const FILE_ACTION_RE = /\b(?:changed|updated|edited|created|deleted|saved|wrote|patched|reviewed|verified|validated|opened|read)\b/i;
|
|
12
17
|
const VERIFIED_PROOF_RE = /^\[verified\]\s+`[^`]+`\s+passed\s+\(exit 0\)(?:\s|$)/i;
|
|
@@ -16,12 +21,29 @@ function compactWhitespace(text) {
|
|
|
16
21
|
return String(text || '').replace(/\s+/g, ' ').trim();
|
|
17
22
|
}
|
|
18
23
|
|
|
24
|
+
function hasSuiteGreenCitation(text) {
|
|
25
|
+
return CI_RUN_CITATION_RE.test(text)
|
|
26
|
+
|| (COMMIT_PINNED_LOCAL_VERIFY_RE.test(text)
|
|
27
|
+
&& COMMAND_PROOF_RE.test(text)
|
|
28
|
+
&& EXIT_ZERO_RE.test(text));
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
function taskProofState(proof) {
|
|
20
32
|
const text = compactWhitespace(proof);
|
|
21
33
|
if (!text) return { ok: false, reason: 'proof required' };
|
|
22
34
|
if (GENERIC_COMPLETION_PROOF_RE.test(text)) {
|
|
23
35
|
return { ok: false, reason: 'proof must name what was verified, changed, approved, or produced' };
|
|
24
36
|
}
|
|
37
|
+
if (SUITE_GREEN_CLAIM_RE.test(text)) {
|
|
38
|
+
if (!hasSuiteGreenCitation(text)) {
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
code: 'suite_green_citation_required',
|
|
42
|
+
reason: SUITE_GREEN_CITATION_REASON,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return { ok: true, reason: 'proof cites a CI run or commit-pinned local verify' };
|
|
46
|
+
}
|
|
25
47
|
if (COMMAND_PROOF_RE.test(text)) return { ok: true, reason: 'proof names a command' };
|
|
26
48
|
if (HUMAN_PROOF_RE.test(text)) return { ok: true, reason: 'proof names human/customer approval' };
|
|
27
49
|
if (RESULT_PAIR_RE.test(text)) return { ok: true, reason: 'proof names a verifier result' };
|
|
@@ -66,7 +88,7 @@ function tailText(text, max = 400) {
|
|
|
66
88
|
// happily pattern-matches even if nothing ran) and a VERIFIED proof: the command
|
|
67
89
|
// actually executed and exited 0. On failure it returns ok:false so the caller
|
|
68
90
|
// can refuse to mark the task ready. The returned proof string is prefixed
|
|
69
|
-
//
|
|
91
|
+
// It names the command and exit; suite-green claims still need a citation.
|
|
70
92
|
function buildVerifiedProof(verifyCmd, baseProof = '', runner, options = {}) {
|
|
71
93
|
const cmd = String(verifyCmd || '').trim();
|
|
72
94
|
if (!cmd) return { ok: false, reason: 'verify_command_required' };
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_FRESHNESS_WINDOW_MS = 15 * 60 * 1000;
|
|
4
|
+
const ACTIVE_TASK_STATES = new Set(['claimed', 'do', 'doing', 'in_progress', 'review']);
|
|
5
|
+
const ACTIVE_MISSION_STATES = new Set(['planning', 'active', 'running', 'ready']);
|
|
6
|
+
|
|
7
|
+
function timestampMs(value) {
|
|
8
|
+
if (value == null || value === '') return 0;
|
|
9
|
+
if (typeof value === 'number') {
|
|
10
|
+
if (!Number.isFinite(value)) return 0;
|
|
11
|
+
return value > 1000000000000 ? value : value * 1000;
|
|
12
|
+
}
|
|
13
|
+
const numeric = Number(value);
|
|
14
|
+
if (Number.isFinite(numeric) && String(value).trim() !== '') return timestampMs(numeric);
|
|
15
|
+
const parsed = Date.parse(String(value));
|
|
16
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function latestTimestamp(...values) {
|
|
20
|
+
return values.reduce((latest, value) => Math.max(latest, timestampMs(value)), 0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isoTimestamp(value) {
|
|
24
|
+
const ms = timestampMs(value);
|
|
25
|
+
return ms ? new Date(ms).toISOString() : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function memberName(value) {
|
|
29
|
+
let text = String(value || '').trim();
|
|
30
|
+
if (!text) return '';
|
|
31
|
+
if (text.includes('/')) text = text.split('/')[0];
|
|
32
|
+
text = text.replace(/[^A-Za-z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
33
|
+
if (!text || /^(team|unknown)$/i.test(text)) return '';
|
|
34
|
+
return text.slice(0, 32);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function operatorSentence(value, fallback = 'current activity was recorded') {
|
|
38
|
+
let text = String(value || '')
|
|
39
|
+
.split(/\r?\n/)
|
|
40
|
+
.map((line) => line.trim().replace(/^[-*]\s+/, ''))
|
|
41
|
+
.find(Boolean) || fallback;
|
|
42
|
+
text = text
|
|
43
|
+
.replace(/(^|\s)--[a-z0-9][a-z0-9-]*(?:=[^\s]+)?/ig, '$1')
|
|
44
|
+
.replace(/\b[0-9A-HJKMNP-TV-Z]{20,26}\b/g, 'the item')
|
|
45
|
+
.replace(/\b[A-Z]{2,12}-\d+\b/gi, 'the task')
|
|
46
|
+
.replace(/\bmission-[a-z0-9][a-z0-9-]{8,}\b/ig, 'the mission')
|
|
47
|
+
.replace(/\s+/g, ' ')
|
|
48
|
+
.trim();
|
|
49
|
+
if (!text) text = fallback;
|
|
50
|
+
if (text.length > 180) text = `${text.slice(0, 177).trim()}...`;
|
|
51
|
+
if (!/[.!?]$/.test(text)) text += '.';
|
|
52
|
+
return text;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function taskOwner(task) {
|
|
56
|
+
return task && (task.claimed_by || task.assigned_to || task.metadata?.assigned_to) || '';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function taskActivityMs(task) {
|
|
60
|
+
const events = Array.isArray(task?.events) ? task.events : [];
|
|
61
|
+
return latestTimestamp(
|
|
62
|
+
task?.updated_at,
|
|
63
|
+
task?.created_at,
|
|
64
|
+
...events.map((event) => event && event.created_at),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function missionActivityMs(mission) {
|
|
69
|
+
return latestTimestamp(
|
|
70
|
+
mission?.last_tick_at,
|
|
71
|
+
mission?.last_tick?.finished_at,
|
|
72
|
+
mission?.last_tick?.at,
|
|
73
|
+
mission?.updated_at,
|
|
74
|
+
mission?.created_at,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function activityOrder(a, b, activityFn) {
|
|
79
|
+
return activityFn(b) - activityFn(a)
|
|
80
|
+
|| String(a.id || a.title || a.objective || '').localeCompare(String(b.id || b.title || b.objective || ''));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function taskRows(input) {
|
|
84
|
+
if (Array.isArray(input.tasks)) return input.tasks;
|
|
85
|
+
if (Array.isArray(input.taskStatus?.tasks)) return input.taskStatus.tasks;
|
|
86
|
+
if (Array.isArray(input.taskStatus?.streams)) {
|
|
87
|
+
return input.taskStatus.streams.flatMap((stream) => Array.isArray(stream.tasks) ? stream.tasks : []);
|
|
88
|
+
}
|
|
89
|
+
return [input.taskStatus?.current].filter(Boolean);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function missionRows(input) {
|
|
93
|
+
if (Array.isArray(input.missions)) return input.missions;
|
|
94
|
+
if (Array.isArray(input.missionStatus?.missions)) return input.missionStatus.missions;
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function loopMissionName(mission) {
|
|
99
|
+
const raw = mission?.name || mission?.title || mission?.objective || mission?.id || 'mission';
|
|
100
|
+
return operatorSentence(raw, 'mission').replace(/[.!?]+$/, '').slice(0, 90);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function loopForMission(mission) {
|
|
104
|
+
if (!mission) return null;
|
|
105
|
+
const lastTick = isoTimestamp(
|
|
106
|
+
mission.last_tick_at
|
|
107
|
+
|| mission.last_tick?.finished_at
|
|
108
|
+
|| mission.last_tick?.at,
|
|
109
|
+
);
|
|
110
|
+
return {
|
|
111
|
+
mission: loopMissionName(mission),
|
|
112
|
+
cadence: String(mission.cadence || 'manual'),
|
|
113
|
+
runner: String(mission.runner || mission.executed_by || 'manual'),
|
|
114
|
+
last_tick: lastTick || 'never',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function streamActiveRows(stream) {
|
|
119
|
+
return (Array.isArray(stream?.active) ? stream.active : []).map((row) => {
|
|
120
|
+
if (Array.isArray(row)) return { name: row[0], summary: row[1], last_seen: row[2] };
|
|
121
|
+
return {
|
|
122
|
+
name: row?.name || row?.agent || row?.member,
|
|
123
|
+
summary: row?.summary || row?.doing || row?.work,
|
|
124
|
+
last_seen: row?.last_seen || row?.ts || row?.at,
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function buildTeamPresence(input = {}) {
|
|
130
|
+
const nowMs = timestampMs(input.nowMs ?? input.now ?? Date.now()) || Date.now();
|
|
131
|
+
const freshnessWindowMs = Number(input.freshnessWindowMs) > 0
|
|
132
|
+
? Number(input.freshnessWindowMs)
|
|
133
|
+
: DEFAULT_FRESHNESS_WINDOW_MS;
|
|
134
|
+
const stream = input.stream || input.streamSnapshot || {};
|
|
135
|
+
const events = Array.isArray(input.streamEvents)
|
|
136
|
+
? input.streamEvents
|
|
137
|
+
: Array.isArray(input.events) ? input.events : [];
|
|
138
|
+
const tasks = taskRows(input).filter(Boolean);
|
|
139
|
+
const missions = missionRows(input).filter(Boolean);
|
|
140
|
+
const candidates = new Map();
|
|
141
|
+
const lastSeen = new Map();
|
|
142
|
+
const summaries = new Map();
|
|
143
|
+
const tasksByMember = new Map();
|
|
144
|
+
const missionsByMember = new Map();
|
|
145
|
+
|
|
146
|
+
const keyFor = (value) => memberName(value).toLowerCase();
|
|
147
|
+
const addCandidate = (value) => {
|
|
148
|
+
const name = memberName(value);
|
|
149
|
+
const key = name.toLowerCase();
|
|
150
|
+
if (!key) return '';
|
|
151
|
+
if (!candidates.has(key)) candidates.set(key, name);
|
|
152
|
+
return key;
|
|
153
|
+
};
|
|
154
|
+
const noteSeen = (value, timestamp) => {
|
|
155
|
+
const key = keyFor(value);
|
|
156
|
+
const ms = timestampMs(timestamp);
|
|
157
|
+
if (!key || !ms) return;
|
|
158
|
+
lastSeen.set(key, Math.max(lastSeen.get(key) || 0, ms));
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
for (const event of events) {
|
|
162
|
+
noteSeen(event?.agent || event?.member || event?.owner, event?.ms || event?.ts || event?.at || event?.created_at);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const row of streamActiveRows(stream)) {
|
|
166
|
+
const key = addCandidate(row.name);
|
|
167
|
+
if (!key) continue;
|
|
168
|
+
if (row.summary) summaries.set(key, row.summary);
|
|
169
|
+
noteSeen(row.name, row.last_seen);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
for (const task of tasks) {
|
|
173
|
+
if (!ACTIVE_TASK_STATES.has(String(task.status || '').toLowerCase())) continue;
|
|
174
|
+
const owner = taskOwner(task);
|
|
175
|
+
const key = addCandidate(owner);
|
|
176
|
+
if (!key) continue;
|
|
177
|
+
noteSeen(owner, taskActivityMs(task));
|
|
178
|
+
if (!tasksByMember.has(key)) tasksByMember.set(key, []);
|
|
179
|
+
tasksByMember.get(key).push(task);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for (const mission of missions) {
|
|
183
|
+
if (!ACTIVE_MISSION_STATES.has(String(mission.status || '').toLowerCase())) continue;
|
|
184
|
+
const owner = mission.owner || mission.member;
|
|
185
|
+
const key = addCandidate(owner);
|
|
186
|
+
if (!key) continue;
|
|
187
|
+
noteSeen(owner, missionActivityMs(mission));
|
|
188
|
+
if (!missionsByMember.has(key)) missionsByMember.set(key, []);
|
|
189
|
+
missionsByMember.get(key).push(mission);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for (const rows of tasksByMember.values()) rows.sort((a, b) => activityOrder(a, b, taskActivityMs));
|
|
193
|
+
for (const rows of missionsByMember.values()) rows.sort((a, b) => activityOrder(a, b, missionActivityMs));
|
|
194
|
+
|
|
195
|
+
const cutoffMs = nowMs - freshnessWindowMs;
|
|
196
|
+
const members = [...candidates.entries()]
|
|
197
|
+
.sort((a, b) => a[1].localeCompare(b[1]))
|
|
198
|
+
.flatMap(([key, name]) => {
|
|
199
|
+
const seenMs = lastSeen.get(key) || 0;
|
|
200
|
+
if (seenMs < cutoffMs) return [];
|
|
201
|
+
const task = tasksByMember.get(key)?.[0] || null;
|
|
202
|
+
const mission = missionsByMember.get(key)?.[0] || null;
|
|
203
|
+
const doing = summaries.get(key)
|
|
204
|
+
|| task?.title
|
|
205
|
+
|| mission?.next_action
|
|
206
|
+
|| mission?.objective;
|
|
207
|
+
return [{
|
|
208
|
+
name,
|
|
209
|
+
awake: true,
|
|
210
|
+
doing: operatorSentence(doing),
|
|
211
|
+
loop: loopForMission(mission),
|
|
212
|
+
last_seen: new Date(seenMs).toISOString(),
|
|
213
|
+
}];
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
schema: 'atris.team_presence.v1',
|
|
218
|
+
generated_at: new Date(nowMs).toISOString(),
|
|
219
|
+
freshness_window_seconds: Math.round(freshnessWindowMs / 1000),
|
|
220
|
+
totals: {
|
|
221
|
+
awake: members.length,
|
|
222
|
+
waiting_operator: Math.max(0, Number(stream.waiting_operator) || 0),
|
|
223
|
+
landing_wait: Math.max(0, Number(stream.landing_wait) || 0),
|
|
224
|
+
},
|
|
225
|
+
members,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function renderTeamPresence(presence) {
|
|
230
|
+
const minutes = presence.freshness_window_seconds / 60;
|
|
231
|
+
const windowText = Number.isInteger(minutes) ? `${minutes} minute${minutes === 1 ? '' : 's'}` : `${presence.freshness_window_seconds} seconds`;
|
|
232
|
+
const lines = [
|
|
233
|
+
`team presence: ${presence.totals.awake} awake`,
|
|
234
|
+
`freshness window: ${windowText}`,
|
|
235
|
+
`waiting on operator: ${presence.totals.waiting_operator}`,
|
|
236
|
+
`landing wait: ${presence.totals.landing_wait}`,
|
|
237
|
+
];
|
|
238
|
+
if (!presence.members.length) {
|
|
239
|
+
lines.push('awake roster: empty');
|
|
240
|
+
return lines.join('\n');
|
|
241
|
+
}
|
|
242
|
+
lines.push('awake roster:');
|
|
243
|
+
for (const member of presence.members) {
|
|
244
|
+
lines.push(` ${member.name}: ${member.doing}`);
|
|
245
|
+
if (member.loop) {
|
|
246
|
+
lines.push(` loop: ${member.loop.mission} [${member.loop.cadence} | ${member.loop.runner} | last tick ${member.loop.last_tick}]`);
|
|
247
|
+
}
|
|
248
|
+
lines.push(` last seen: ${member.last_seen}`);
|
|
249
|
+
}
|
|
250
|
+
return lines.join('\n');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = {
|
|
254
|
+
ACTIVE_MISSION_STATES,
|
|
255
|
+
ACTIVE_TASK_STATES,
|
|
256
|
+
DEFAULT_FRESHNESS_WINDOW_MS,
|
|
257
|
+
buildTeamPresence,
|
|
258
|
+
operatorSentence,
|
|
259
|
+
renderTeamPresence,
|
|
260
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const HISTORY_FILES = [
|
|
7
|
+
path.join('.atris', 'state', 'career_xp_receipts.jsonl'),
|
|
8
|
+
path.join('.atris', 'state', 'scorecards.jsonl'),
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const PASS_OUTCOMES = new Set(['accepted', 'approved', 'done', 'pass', 'passed', 'success', 'succeeded']);
|
|
12
|
+
const FAIL_OUTCOMES = new Set(['bounced', 'fail', 'failed', 'rejected', 'revised', 'rework_requested']);
|
|
13
|
+
|
|
14
|
+
function normalizedActor(value) {
|
|
15
|
+
return String(value || '').trim().toLowerCase();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function rowActor(row) {
|
|
19
|
+
return normalizedActor(row?.claimed_by || row?.metadata?.executed_by);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function rowPassed(row) {
|
|
23
|
+
for (const value of [row?.passed, row?.verify_passed, row?.metadata?.verify_passed]) {
|
|
24
|
+
if (typeof value === 'boolean') return value;
|
|
25
|
+
}
|
|
26
|
+
const outcome = String(row?.outcome || row?.status || '').trim().toLowerCase();
|
|
27
|
+
if (PASS_OUTCOMES.has(outcome)) return true;
|
|
28
|
+
if (FAIL_OUTCOMES.has(outcome)) return false;
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function rowTime(row) {
|
|
33
|
+
for (const value of [row?.accepted_at, row?.ts, row?.recorded_at, row?.created_at, row?.updated_at]) {
|
|
34
|
+
const parsed = Date.parse(value || '');
|
|
35
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readHistory(root) {
|
|
41
|
+
const rows = [];
|
|
42
|
+
let sequence = 0;
|
|
43
|
+
for (const relative of HISTORY_FILES) {
|
|
44
|
+
let text;
|
|
45
|
+
try {
|
|
46
|
+
text = fs.readFileSync(path.join(root, relative), 'utf8');
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
for (const line of text.split(/\r?\n/)) {
|
|
51
|
+
if (!line.trim()) continue;
|
|
52
|
+
let row;
|
|
53
|
+
try {
|
|
54
|
+
row = JSON.parse(line);
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) return null;
|
|
59
|
+
rows.push({ row, sequence: sequence++, time: rowTime(row) });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return rows;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function computeTrustTier(actor, root = process.cwd()) {
|
|
66
|
+
const target = normalizedActor(actor);
|
|
67
|
+
if (!target) return 'probation';
|
|
68
|
+
const history = readHistory(root);
|
|
69
|
+
if (!history) return 'probation';
|
|
70
|
+
|
|
71
|
+
const outcomes = history
|
|
72
|
+
.filter(({ row }) => rowActor(row) === target)
|
|
73
|
+
.map((entry) => ({ ...entry, passed: rowPassed(entry.row) }))
|
|
74
|
+
.filter(({ passed }) => passed !== null)
|
|
75
|
+
.sort((a, b) => {
|
|
76
|
+
if (a.time !== null && b.time !== null && a.time !== b.time) return a.time - b.time;
|
|
77
|
+
if (a.time !== null && b.time === null) return 1;
|
|
78
|
+
if (a.time === null && b.time !== null) return -1;
|
|
79
|
+
return a.sequence - b.sequence;
|
|
80
|
+
})
|
|
81
|
+
.slice(-20);
|
|
82
|
+
|
|
83
|
+
const passed = outcomes.filter((outcome) => outcome.passed).length;
|
|
84
|
+
const passRate = outcomes.length ? passed / outcomes.length : 0;
|
|
85
|
+
if (outcomes.length >= 10 && passRate >= 0.9) return 'trusted';
|
|
86
|
+
if (outcomes.length >= 5 && passRate >= 0.7) return 'standard';
|
|
87
|
+
return 'probation';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { computeTrustTier };
|