atris 3.80.1 → 3.82.2
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/LICENSE +21 -0
- package/README.md +11 -1
- package/atris/atris.md +14 -0
- package/atris/features/README.md +15 -0
- package/atris/policies/README.md +24 -0
- package/atris/skills/README.md +61 -45
- package/atris/skills/design/SKILL.md +6 -5
- package/atris/skills/taste/SKILL.md +39 -0
- package/atris/skills/taste/notes.md +29 -0
- package/atris/skills/taste/principles.md +23 -0
- package/atris/skills/wiki/SKILL.md +14 -2
- package/atris/skills/wiki/index-lines.md +30 -0
- package/atris/skills/wiki/measure.md +20 -0
- package/atris/skills/wiki/note-shape.md +16 -0
- package/atris/skills/wiki/upkeep.md +16 -0
- package/atris/team/README.md +38 -0
- package/atris.md +14 -0
- package/bin/atris.js +1 -1
- package/commands/activate.js +1 -1
- package/commands/autoland.js +25 -26
- package/commands/autopilot-front.js +46 -1
- package/commands/autopilot.js +55 -25
- package/commands/clean.js +22 -11
- package/commands/computer.js +1 -1
- package/commands/doc-health.js +48 -1
- package/commands/improve.js +16 -3
- package/commands/init.js +4 -0
- package/commands/integrations.js +69 -1616
- package/commands/mission.js +112 -14
- package/commands/release.js +41 -10
- package/commands/status.js +1 -1
- package/commands/sync.js +12 -8
- package/commands/task.js +150 -137
- package/commands/unknowns.js +9 -5
- package/commands/visualize.js +1 -12
- package/commands/workflow.js +4 -3
- package/commands/worktree.js +107 -1610
- package/commands/xp.js +23 -2113
- package/commands/youtube.js +192 -3110
- package/lib/business-core.js +3 -4
- package/lib/campaign-run-lock.js +12 -1
- package/lib/engine-ask.js +1 -1
- package/lib/engine-receipt-sweep.js +64 -0
- package/lib/file-lock.js +175 -0
- package/lib/first-minute.js +37 -4
- package/lib/fleet.js +1 -1
- package/lib/integrations-core.js +1695 -0
- package/lib/intents.js +7 -0
- package/lib/json-file.js +22 -1
- package/lib/loop-doctor.js +36 -2
- package/lib/map-refs.js +68 -8
- package/lib/member-context.js +20 -1
- package/lib/member-engine.js +1 -1
- package/lib/member-map-fold.js +49 -11
- package/lib/member-map-inbox.js +2 -1
- package/lib/mission-core.js +9 -2
- package/lib/next-work-context.js +190 -0
- package/lib/recall-exam.js +171 -0
- package/lib/roster-markdown.js +2 -2
- package/lib/runner-command.js +5 -5
- package/lib/task-db.js +107 -38
- package/lib/taste-notes.js +35 -0
- package/lib/team-lineup.js +3 -3
- package/lib/todo.js +2 -0
- package/lib/wiki.js +11 -3
- package/lib/worktree-core.js +2032 -0
- package/lib/xp-core.js +2172 -0
- package/lib/youtube-core.js +3178 -0
- package/lib/zen-apply.js +3 -1
- package/package.json +2 -2
package/commands/task.js
CHANGED
|
@@ -10,6 +10,7 @@ const { execSync } = require('node:child_process');
|
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const os = require('os');
|
|
12
12
|
const { hasFlag } = require('../lib/arg-parser');
|
|
13
|
+
const { buildNextWorkContext, reconcileTaskRows } = require('../lib/next-work-context');
|
|
13
14
|
const { argsWantHelp } = require('../lib/noninteractive');
|
|
14
15
|
const {
|
|
15
16
|
compactErrorPayload,
|
|
@@ -47,6 +48,7 @@ const {
|
|
|
47
48
|
} = require('../lib/task-explanation');
|
|
48
49
|
const { extractReceiptEvidence, RECEIPT_PATH_PATTERN } = require('../lib/receipt-evidence');
|
|
49
50
|
const escapeRegExp = require('../lib/escape-regexp');
|
|
51
|
+
const { writeFileAtomic } = require('../lib/json-file');
|
|
50
52
|
const reviewIntegrity = require('../lib/review-integrity');
|
|
51
53
|
const { gateForHuman, isRetiredFillerReason, landingWhyClause, numberWord, plainLandingReason } = require('../lib/voice-gate');
|
|
52
54
|
const {
|
|
@@ -73,6 +75,7 @@ const {
|
|
|
73
75
|
} = require('../lib/task-decision');
|
|
74
76
|
const {
|
|
75
77
|
buildFirstMinute,
|
|
78
|
+
loadLocalTaskState,
|
|
76
79
|
deskNextCommand,
|
|
77
80
|
firstTalkCommand,
|
|
78
81
|
folderName,
|
|
@@ -184,14 +187,15 @@ function writeDefaultProjection(taskDb, db, options = {}) {
|
|
|
184
187
|
}));
|
|
185
188
|
const outPath = path.resolve(root || process.cwd(), '.atris', 'state', 'tasks.projection.json');
|
|
186
189
|
const output = JSON.stringify(projection, null, 2) + '\n';
|
|
187
|
-
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
188
190
|
let shouldWrite = true;
|
|
189
191
|
try {
|
|
190
192
|
shouldWrite = fs.readFileSync(outPath, 'utf8') !== output;
|
|
191
193
|
} catch {
|
|
192
194
|
shouldWrite = true;
|
|
193
195
|
}
|
|
194
|
-
|
|
196
|
+
// Temp file and rename: a reader never sees half a projection. This is the one writer of the
|
|
197
|
+
// projection's shape; lib/mission-core.js writes through it too.
|
|
198
|
+
if (shouldWrite) writeFileAtomic(outPath, output);
|
|
195
199
|
return { projection, outPath };
|
|
196
200
|
}
|
|
197
201
|
|
|
@@ -4787,16 +4791,13 @@ function cmdReviews(args) {
|
|
|
4787
4791
|
}
|
|
4788
4792
|
|
|
4789
4793
|
function stampAcceptGroupMetadata(taskDb, db, taskId, { actor, group, verified, sampleVerifiedIds }) {
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
if (!verified) metadata.sample_verified_ids = sampleVerifiedIds;
|
|
4798
|
-
db.prepare('UPDATE tasks SET metadata = ?, updated_at = ? WHERE id = ?')
|
|
4799
|
-
.run(JSON.stringify(metadata), Date.now(), taskId);
|
|
4794
|
+
taskDb.updateTaskMetadata(db, taskId, (metadata) => {
|
|
4795
|
+
metadata.accept_group = group;
|
|
4796
|
+
metadata.accept_group_by = actor;
|
|
4797
|
+
metadata.accept_group_at = new Date().toISOString();
|
|
4798
|
+
metadata.accepted_by_sampling = !verified;
|
|
4799
|
+
if (!verified) metadata.sample_verified_ids = sampleVerifiedIds;
|
|
4800
|
+
});
|
|
4800
4801
|
}
|
|
4801
4802
|
|
|
4802
4803
|
// Accept a whole certified cluster honestly: the human spot-checks K rows (real career XP), the rest
|
|
@@ -4902,16 +4903,7 @@ function cmdAcceptGroup(args) {
|
|
|
4902
4903
|
accepted.push({ id: task.id, ok: false, reason: MISSION_XP_END_TO_END_REASON, detail: missionXpIssue });
|
|
4903
4904
|
continue;
|
|
4904
4905
|
}
|
|
4905
|
-
const done = taskDb.
|
|
4906
|
-
id: task.id,
|
|
4907
|
-
status: 'done',
|
|
4908
|
-
actor,
|
|
4909
|
-
allowReview: true,
|
|
4910
|
-
action: 'accepted',
|
|
4911
|
-
proof,
|
|
4912
|
-
});
|
|
4913
|
-
if (!done.updated) { accepted.push({ id: task.id, ok: false, reason: 'not_review' }); continue; }
|
|
4914
|
-
const reviewed = taskDb.reviewTask(db, {
|
|
4906
|
+
const done = taskDb.acceptTask(db, {
|
|
4915
4907
|
id: task.id,
|
|
4916
4908
|
actor,
|
|
4917
4909
|
reward: 1,
|
|
@@ -4920,6 +4912,8 @@ function cmdAcceptGroup(args) {
|
|
|
4920
4912
|
proof,
|
|
4921
4913
|
careerXpEligible: isVerified, // honest: career XP ONLY for rows the human actually verified
|
|
4922
4914
|
});
|
|
4915
|
+
if (!done.updated) { accepted.push({ id: task.id, ok: false, reason: 'not_review' }); continue; }
|
|
4916
|
+
const reviewed = done.reviewed;
|
|
4923
4917
|
refreshCareerXpAfterReview(reviewed);
|
|
4924
4918
|
stampAcceptGroupMetadata(taskDb, db, task.id, { actor, group: groupLabel, verified: isVerified, sampleVerifiedIds: [...verifiedIds] });
|
|
4925
4919
|
accepted.push({ id: task.id, ok: true, verified: isVerified, career_xp: isVerified });
|
|
@@ -5888,27 +5882,11 @@ function cmdNextTruth(args) {
|
|
|
5888
5882
|
const taskDb = getTaskDb();
|
|
5889
5883
|
const db = taskDb.open();
|
|
5890
5884
|
const workspaceRoot = taskDb.workspaceRoot();
|
|
5891
|
-
const
|
|
5892
|
-
const existingProj = readProjectionFile(workspaceRoot);
|
|
5893
|
-
const dbHasActionable = rows.some((task) => {
|
|
5894
|
-
const status = task && task.status;
|
|
5895
|
-
return status === 'open' || status === 'claimed' || status === 'review';
|
|
5896
|
-
});
|
|
5897
|
-
const projHasActionable = Boolean(
|
|
5898
|
-
existingProj
|
|
5899
|
-
&& Array.isArray(existingProj.tasks)
|
|
5900
|
-
&& existingProj.tasks.some((task) => {
|
|
5901
|
-
const status = task && task.status;
|
|
5902
|
-
return status === 'open' || status === 'claimed' || status === 'review';
|
|
5903
|
-
}),
|
|
5904
|
-
);
|
|
5885
|
+
const snapshot = loadLocalTaskState(workspaceRoot);
|
|
5905
5886
|
let projection;
|
|
5906
5887
|
let outPath;
|
|
5907
|
-
if (
|
|
5908
|
-
projection =
|
|
5909
|
-
outPath = path.resolve(path.join(workspaceRoot || '.', '.atris', 'state', 'tasks.projection.json'));
|
|
5910
|
-
} else if (rows.length === 0 && existingProj && Array.isArray(existingProj.tasks) && existingProj.tasks.length > 0) {
|
|
5911
|
-
projection = existingProj;
|
|
5888
|
+
if (snapshot.source !== 'database') {
|
|
5889
|
+
projection = { tasks: snapshot.tasks };
|
|
5912
5890
|
outPath = path.resolve(path.join(workspaceRoot || '.', '.atris', 'state', 'tasks.projection.json'));
|
|
5913
5891
|
} else {
|
|
5914
5892
|
const written = writeDefaultProjection(taskDb, db);
|
|
@@ -5936,6 +5914,7 @@ function cmdNextTruth(args) {
|
|
|
5936
5914
|
scope: normalizeTaskQueueScope(scope),
|
|
5937
5915
|
projection_path: outPath,
|
|
5938
5916
|
task: task ? compactTaskFromProjection(projection, task.id) : null,
|
|
5917
|
+
context: buildNextWorkContext({ root: workspaceRoot, tasks: reconcileTaskRows(snapshot.tasks, projection.tasks || []), selected: task, owner: String(owner), taskSource: snapshot.source, generatedAt: snapshot.generated_at }),
|
|
5939
5918
|
});
|
|
5940
5919
|
return;
|
|
5941
5920
|
}
|
|
@@ -9390,19 +9369,8 @@ async function cmdAccept(args) {
|
|
|
9390
9369
|
console.error('atris task accept: reward must be a positive number');
|
|
9391
9370
|
process.exit(2);
|
|
9392
9371
|
}
|
|
9393
|
-
|
|
9394
|
-
|
|
9395
|
-
status: 'done',
|
|
9396
|
-
actor,
|
|
9397
|
-
allowReview: true,
|
|
9398
|
-
action: 'accepted',
|
|
9399
|
-
proof,
|
|
9400
|
-
});
|
|
9401
|
-
if (!done.updated) {
|
|
9402
|
-
console.error(`accept failed: ${taskId} not open|claimed|review`);
|
|
9403
|
-
process.exit(1);
|
|
9404
|
-
}
|
|
9405
|
-
const reviewed = taskDb.reviewTask(db, {
|
|
9372
|
+
// One transaction: done and accepted land together or not at all (lib/task-db.js acceptTask).
|
|
9373
|
+
const done = taskDb.acceptTask(db, {
|
|
9406
9374
|
id: taskId,
|
|
9407
9375
|
actor,
|
|
9408
9376
|
reward: parsedReward.value,
|
|
@@ -9412,6 +9380,11 @@ async function cmdAccept(args) {
|
|
|
9412
9380
|
careerXpEligible: true,
|
|
9413
9381
|
clearedFields,
|
|
9414
9382
|
});
|
|
9383
|
+
if (!done.updated) {
|
|
9384
|
+
console.error(`accept failed: ${taskId} not open|claimed|review`);
|
|
9385
|
+
process.exit(1);
|
|
9386
|
+
}
|
|
9387
|
+
const reviewed = done.reviewed;
|
|
9415
9388
|
const xpProjection = refreshCareerXpAfterReview(reviewed);
|
|
9416
9389
|
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
9417
9390
|
const workspaceRoot = projection.workspace_root || process.cwd();
|
|
@@ -9485,17 +9458,13 @@ function stampAutoAcceptMetadata(taskDb, db, task, actor, policy) {
|
|
|
9485
9458
|
} catch (err) {
|
|
9486
9459
|
landing = { status: 'unverified', reason: 'landing_proof_failed', detail: String((err && err.message) || err).slice(0, 200) };
|
|
9487
9460
|
}
|
|
9488
|
-
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
|
|
9493
|
-
|
|
9494
|
-
|
|
9495
|
-
SET metadata = ?,
|
|
9496
|
-
updated_at = ?
|
|
9497
|
-
WHERE id = ?
|
|
9498
|
-
`).run(JSON.stringify(metadata), Date.now(), taskId);
|
|
9461
|
+
// The landing proof runs git, so it is worked out first; only the write holds the lock.
|
|
9462
|
+
taskDb.updateTaskMetadata(db, taskId, (metadata) => {
|
|
9463
|
+
metadata.auto_accepted_at = new Date().toISOString();
|
|
9464
|
+
metadata.auto_accepted_by = actor;
|
|
9465
|
+
metadata.auto_accept_policy = policy;
|
|
9466
|
+
metadata.landing = { ...landing, checked_at: metadata.auto_accepted_at };
|
|
9467
|
+
});
|
|
9499
9468
|
return landing;
|
|
9500
9469
|
}
|
|
9501
9470
|
|
|
@@ -9512,19 +9481,8 @@ function acceptReviewTask(taskDb, db, taskId, { actor, proof, reward, lesson = '
|
|
|
9512
9481
|
if (missionXpIssue) {
|
|
9513
9482
|
return { ok: false, reason: MISSION_XP_END_TO_END_REASON, detail: missionXpIssue };
|
|
9514
9483
|
}
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
status: 'done',
|
|
9518
|
-
actor,
|
|
9519
|
-
allowReview: true,
|
|
9520
|
-
action: 'accepted',
|
|
9521
|
-
proof,
|
|
9522
|
-
autoAccepted,
|
|
9523
|
-
});
|
|
9524
|
-
if (!done.updated) {
|
|
9525
|
-
return { ok: false, reason: 'not_open_claimed_or_review' };
|
|
9526
|
-
}
|
|
9527
|
-
const reviewed = taskDb.reviewTask(db, {
|
|
9484
|
+
// One transaction: done and accepted land together or not at all (lib/task-db.js acceptTask).
|
|
9485
|
+
const accepted = taskDb.acceptTask(db, {
|
|
9528
9486
|
id: taskId,
|
|
9529
9487
|
actor,
|
|
9530
9488
|
reward,
|
|
@@ -9534,6 +9492,10 @@ function acceptReviewTask(taskDb, db, taskId, { actor, proof, reward, lesson = '
|
|
|
9534
9492
|
careerXpEligible: true,
|
|
9535
9493
|
autoAccepted,
|
|
9536
9494
|
});
|
|
9495
|
+
if (!accepted.updated) {
|
|
9496
|
+
return { ok: false, reason: 'not_open_claimed_or_review' };
|
|
9497
|
+
}
|
|
9498
|
+
const reviewed = accepted.reviewed;
|
|
9537
9499
|
const acceptedRow = taskDb.getTask(db, taskId);
|
|
9538
9500
|
refreshExistingTodoMarkdown(taskDb, db, acceptedRow && acceptedRow.workspace_root);
|
|
9539
9501
|
return { ok: true, reviewed };
|
|
@@ -9580,19 +9542,12 @@ function verifyIsOnlyFileChecks(verify) {
|
|
|
9580
9542
|
}
|
|
9581
9543
|
|
|
9582
9544
|
function stampCertifyVerifyMetadata(taskDb, db, taskId, actor, verify) {
|
|
9583
|
-
|
|
9584
|
-
|
|
9585
|
-
|
|
9586
|
-
|
|
9587
|
-
|
|
9588
|
-
|
|
9589
|
-
metadata.machine_verified = true;
|
|
9590
|
-
db.prepare(`
|
|
9591
|
-
UPDATE tasks
|
|
9592
|
-
SET metadata = ?,
|
|
9593
|
-
updated_at = ?
|
|
9594
|
-
WHERE id = ?
|
|
9595
|
-
`).run(JSON.stringify(metadata), Date.now(), taskId);
|
|
9545
|
+
taskDb.updateTaskMetadata(db, taskId, (metadata) => {
|
|
9546
|
+
metadata.verify = metadata.verify || verify;
|
|
9547
|
+
metadata.certified_verified_at = new Date().toISOString();
|
|
9548
|
+
metadata.certified_verified_by = actor;
|
|
9549
|
+
metadata.machine_verified = true;
|
|
9550
|
+
});
|
|
9596
9551
|
}
|
|
9597
9552
|
|
|
9598
9553
|
function currentHeadSha(workspaceRoot) {
|
|
@@ -9613,27 +9568,23 @@ function currentHeadSha(workspaceRoot) {
|
|
|
9613
9568
|
// certify-verified and the autoland landing re-check, persist the outcome
|
|
9614
9569
|
// here for the read path to consult.
|
|
9615
9570
|
function stampVerifyCacheMetadata(taskDb, db, taskId, { verify, result, actor, workspaceRoot }) {
|
|
9616
|
-
|
|
9617
|
-
|
|
9618
|
-
const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
|
|
9619
|
-
metadata.verify_cache = {
|
|
9571
|
+
if (!taskDb.getTask(db, taskId)) return;
|
|
9572
|
+
const verifyCache = {
|
|
9620
9573
|
command: verify,
|
|
9621
9574
|
ok: result && result.ok === true,
|
|
9622
9575
|
reason: (result && result.reason) || null,
|
|
9623
9576
|
ran_at: new Date().toISOString(),
|
|
9624
9577
|
ran_by: actor,
|
|
9625
9578
|
// The commit the check ran on: the job's own code when it has some,
|
|
9626
|
-
// else the folder the job lives in.
|
|
9579
|
+
// else the folder the job lives in. Read before the write lock is taken.
|
|
9627
9580
|
code_sha: (result && result.code_sha) || currentHeadSha(workspaceRoot),
|
|
9628
9581
|
code_source: (result && result.code_source) || 'checkout',
|
|
9629
9582
|
};
|
|
9630
9583
|
// A check is not a touch. Bumping updated_at here put every failed job at
|
|
9631
9584
|
// the front of the next pass, so the same six failing jobs won every hour.
|
|
9632
|
-
|
|
9633
|
-
|
|
9634
|
-
|
|
9635
|
-
WHERE id = ?
|
|
9636
|
-
`).run(JSON.stringify(metadata), taskId);
|
|
9585
|
+
taskDb.updateTaskMetadata(db, taskId, (metadata) => {
|
|
9586
|
+
metadata.verify_cache = verifyCache;
|
|
9587
|
+
}, { touch: false });
|
|
9637
9588
|
}
|
|
9638
9589
|
|
|
9639
9590
|
// When a review job was last given a second check; never checked reads 0.
|
|
@@ -9656,23 +9607,16 @@ function failedOnSameCode(task, verify, codeSha) {
|
|
|
9656
9607
|
// metadata.verify is what lets sweep --auto-accept (and certify-verified)
|
|
9657
9608
|
// re-run it later against the current checkout instead of re-parsing prose.
|
|
9658
9609
|
function stampReadyVerifyMetadata(taskDb, db, taskId, verify) {
|
|
9659
|
-
|
|
9660
|
-
|
|
9661
|
-
|
|
9662
|
-
|
|
9663
|
-
|
|
9664
|
-
|
|
9665
|
-
|
|
9666
|
-
|
|
9667
|
-
|
|
9668
|
-
|
|
9669
|
-
}
|
|
9670
|
-
db.prepare(`
|
|
9671
|
-
UPDATE tasks
|
|
9672
|
-
SET metadata = ?,
|
|
9673
|
-
updated_at = ?
|
|
9674
|
-
WHERE id = ?
|
|
9675
|
-
`).run(JSON.stringify(metadata), Date.now(), taskId);
|
|
9610
|
+
taskDb.updateTaskMetadata(db, taskId, (metadata) => {
|
|
9611
|
+
metadata.verify = verify;
|
|
9612
|
+
// A real verify stamped at ready means the creation-time degraded flag is
|
|
9613
|
+
// stale: clear it so `task show` stops warning about a missing check. A
|
|
9614
|
+
// diff-only verify keeps the flag; it still cannot fail on real breakage.
|
|
9615
|
+
if (verify.toLowerCase() !== 'git diff --check') {
|
|
9616
|
+
delete metadata.verification_status;
|
|
9617
|
+
delete metadata.verification_degraded_reason;
|
|
9618
|
+
}
|
|
9619
|
+
});
|
|
9676
9620
|
}
|
|
9677
9621
|
|
|
9678
9622
|
function landingVerifyFailureNote(verify, result) {
|
|
@@ -11095,13 +11039,29 @@ function renderedTaskRefSet(taskDb, rows, refRows) {
|
|
|
11095
11039
|
return refs;
|
|
11096
11040
|
}
|
|
11097
11041
|
|
|
11098
|
-
|
|
11042
|
+
// A bullet the renderer wrote starts with its job's ref in bold brackets: `- **[WSX-1]** title`.
|
|
11043
|
+
const RENDERED_TODO_BULLET_REF = /^\s*- (?:\[[ x]\]\s+)?\*\*\[([^\]]+)\]\*\*/;
|
|
11044
|
+
|
|
11045
|
+
// Rows in TODO.md that the database does not know. In a hand-written TODO they are rendered
|
|
11046
|
+
// from the markdown, as before. In a rendered TODO (it has the "Regenerated" marker) a bullet
|
|
11047
|
+
// the renderer wrote whose job is gone stays gone, but a bullet a person or a command added by
|
|
11048
|
+
// hand (no bracketed ref, like `atris business` writing `- **Onboard:** ...`) is imported into
|
|
11049
|
+
// the database when `db` is given, so the next render keeps it instead of dropping it. Imported
|
|
11050
|
+
// rows are pushed onto `rows` and `refRows`, so the caller renders them with the rest.
|
|
11051
|
+
function markdownRowsForRender(taskDb, existingTodoPath, rows, refRows, { db = null, workspaceRoot: wsOption = null } = {}) {
|
|
11099
11052
|
if (!existingTodoPath || !fs.existsSync(existingTodoPath)) return [];
|
|
11100
11053
|
const { parseTodoFile } = require('../lib/todo-fallback');
|
|
11101
11054
|
const existingTodo = fs.readFileSync(existingTodoPath, 'utf8');
|
|
11102
11055
|
const generatedTodo = existingTodo.includes('Regenerated from durable Atris task state');
|
|
11103
11056
|
const parsed = parseTodoFile(existingTodoPath);
|
|
11104
|
-
const ws = taskDb.workspaceRoot();
|
|
11057
|
+
const ws = wsOption || taskDb.workspaceRoot();
|
|
11058
|
+
const renderedBulletRefs = new Set();
|
|
11059
|
+
if (generatedTodo) {
|
|
11060
|
+
for (const line of existingTodo.split('\n')) {
|
|
11061
|
+
const match = line.match(RENDERED_TODO_BULLET_REF);
|
|
11062
|
+
if (match) renderedBulletRefs.add(normalizeRenderedTaskRef(match[1]));
|
|
11063
|
+
}
|
|
11064
|
+
}
|
|
11105
11065
|
const existingRefs = renderedTaskRefSet(taskDb, rows, refRows);
|
|
11106
11066
|
const existingSourceKeys = new Set(
|
|
11107
11067
|
(Array.isArray(refRows) ? refRows : [])
|
|
@@ -11130,9 +11090,38 @@ function markdownRowsForRender(taskDb, existingTodoPath, rows, refRows) {
|
|
|
11130
11090
|
if (
|
|
11131
11091
|
(renderedRef && existingRefs.has(renderedRef)) ||
|
|
11132
11092
|
(sk && existingSourceKeys.has(sk)) ||
|
|
11133
|
-
existingTitles.has(normalizedTitle)
|
|
11134
|
-
generatedTodo
|
|
11093
|
+
existingTitles.has(normalizedTitle)
|
|
11135
11094
|
) continue;
|
|
11095
|
+
if (generatedTodo) {
|
|
11096
|
+
// Written by the renderer and no longer in the database: leave it out.
|
|
11097
|
+
// Only open and in-progress bullets are imported: a hand-written line under Review or
|
|
11098
|
+
// Completed has no history to stand on.
|
|
11099
|
+
if (!db || (renderedRef && renderedBulletRefs.has(renderedRef)) || (status !== 'open' && status !== 'claimed')) continue;
|
|
11100
|
+
const claimer = status === 'claimed' ? (task.claimed || null) : null;
|
|
11101
|
+
const added = taskDb.addTask(db, {
|
|
11102
|
+
title: task.title,
|
|
11103
|
+
tag: task.tag || null,
|
|
11104
|
+
workspaceRoot: ws,
|
|
11105
|
+
sourceKey: sk,
|
|
11106
|
+
status: status === 'claimed' && !claimer ? 'open' : status,
|
|
11107
|
+
claimedBy: claimer,
|
|
11108
|
+
metadata: {
|
|
11109
|
+
source: 'todo_markdown_import',
|
|
11110
|
+
todo_id: task.id || null,
|
|
11111
|
+
todo_tags: task.tags || [],
|
|
11112
|
+
...(task.verify ? { verify: task.verify } : {}),
|
|
11113
|
+
},
|
|
11114
|
+
});
|
|
11115
|
+
const imported = added && added.id ? taskDb.getTask(db, added.id) : null;
|
|
11116
|
+
if (imported) {
|
|
11117
|
+
if (Array.isArray(rows)) rows.push(imported);
|
|
11118
|
+
if (Array.isArray(refRows) && refRows !== rows) refRows.push(imported);
|
|
11119
|
+
existingRefs.add(normalizeRenderedTaskRef(imported.id));
|
|
11120
|
+
}
|
|
11121
|
+
if (sk) existingSourceKeys.add(sk);
|
|
11122
|
+
existingTitles.add(normalizedTitle);
|
|
11123
|
+
continue;
|
|
11124
|
+
}
|
|
11136
11125
|
out.push({
|
|
11137
11126
|
id: `markdown:${status}:${task.id || index}:${sk ? sk.slice(0, 10) : index}`,
|
|
11138
11127
|
title: task.title,
|
|
@@ -11204,12 +11193,12 @@ function refreshExistingTodoMarkdown(taskDb, db, workspaceRoot) {
|
|
|
11204
11193
|
const preservedSections = [];
|
|
11205
11194
|
const endgameSection = extractTodoSectionMarkdown(existingTodo, 'Endgame');
|
|
11206
11195
|
if (endgameSection) preservedSections.push(endgameSection);
|
|
11207
|
-
const markdownRows = markdownRowsForRender(taskDb, outPath, rows, refRows);
|
|
11196
|
+
const markdownRows = markdownRowsForRender(taskDb, outPath, rows, refRows, { db, workspaceRoot: ws });
|
|
11208
11197
|
const markdown = taskDb.renderTodoMarkdown([...rows, ...markdownRows], {
|
|
11209
11198
|
refRows,
|
|
11210
11199
|
preservedSections,
|
|
11211
11200
|
});
|
|
11212
|
-
|
|
11201
|
+
writeFileAtomic(outPath, markdown);
|
|
11213
11202
|
return outPath;
|
|
11214
11203
|
}
|
|
11215
11204
|
|
|
@@ -11225,13 +11214,43 @@ function autoRenderTodoFromDb(cwd = process.cwd()) {
|
|
|
11225
11214
|
if (fs.existsSync(outPath)) return refreshExistingTodoMarkdown(taskDb, db, cwd);
|
|
11226
11215
|
const rows = taskDb.listTasks(db, { workspaceRoot: cwd, limit: 500 });
|
|
11227
11216
|
const refRows = taskDb.listTasks(db, { workspaceRoot: cwd });
|
|
11228
|
-
|
|
11217
|
+
writeFileAtomic(outPath, taskDb.renderTodoMarkdown(rows, { refRows }));
|
|
11229
11218
|
return outPath;
|
|
11230
11219
|
} catch {
|
|
11231
11220
|
return null;
|
|
11232
11221
|
}
|
|
11233
11222
|
}
|
|
11234
11223
|
|
|
11224
|
+
// The task behind a TODO bullet, finished the way `atris task done` finishes one: doneTask, then the
|
|
11225
|
+
// projection and the rendered TODO. A rendered TODO.md is a view of the database, so marking its bullet
|
|
11226
|
+
// means nothing until the job itself is done. `matches(title)` says whether a job's title is the one the
|
|
11227
|
+
// caller named; only an open or claimed job in this workspace counts, an exact title wins over a partial
|
|
11228
|
+
// one, and two partial matches are a refusal, not a guess. Returns { completed, id, reason }.
|
|
11229
|
+
function completeTodoTaskByTitle(cwd, title, { matches, actor, proof } = {}) {
|
|
11230
|
+
const taskDb = getTaskDb();
|
|
11231
|
+
const db = taskDb.open();
|
|
11232
|
+
const ws = taskDb.workspaceRoot(cwd);
|
|
11233
|
+
const wanted = taskDb.normalizeTitle(title);
|
|
11234
|
+
const candidates = taskDb.listTasks(db, { workspaceRoot: ws, limit: null })
|
|
11235
|
+
.filter((row) => row.status === 'open' || row.status === 'claimed');
|
|
11236
|
+
const exact = candidates.filter((row) => wanted && taskDb.normalizeTitle(row.title) === wanted);
|
|
11237
|
+
const partial = typeof matches === 'function' ? candidates.filter((row) => matches(row.title)) : [];
|
|
11238
|
+
const picked = exact.length === 1 ? exact : (exact.length ? [] : partial);
|
|
11239
|
+
if (picked.length !== 1) return { completed: false, id: null, reason: picked.length ? 'ambiguous' : 'no_match' };
|
|
11240
|
+
const id = picked[0].id;
|
|
11241
|
+
const done = taskDb.doneTask(db, {
|
|
11242
|
+
id,
|
|
11243
|
+
status: 'done',
|
|
11244
|
+
actor: actor || process.env.ATRIS_AGENT_ID || DEFAULT_OWNER,
|
|
11245
|
+
action: 'done',
|
|
11246
|
+
proof,
|
|
11247
|
+
});
|
|
11248
|
+
if (!done.updated) return { completed: false, id, reason: 'not_open_or_claimed' };
|
|
11249
|
+
try { writeDefaultProjection(taskDb, db, { root: cwd }); } catch { /* the view is rebuilt on the next write */ }
|
|
11250
|
+
autoRenderTodoFromDb(cwd);
|
|
11251
|
+
return { completed: true, id, reason: null };
|
|
11252
|
+
}
|
|
11253
|
+
|
|
11235
11254
|
function cmdRender(args) {
|
|
11236
11255
|
const out = flag(args, '--out') || path.join('atris', 'TODO.md');
|
|
11237
11256
|
const all = hasFlag(args, '--all');
|
|
@@ -11255,12 +11274,12 @@ function cmdRender(args) {
|
|
|
11255
11274
|
const preservedSections = [];
|
|
11256
11275
|
const endgameSection = extractTodoSectionMarkdown(existingTodo, 'Endgame');
|
|
11257
11276
|
if (endgameSection) preservedSections.push(endgameSection);
|
|
11258
|
-
const markdownRows = markdownRowsForRender(taskDb, outPath, rows, refRows);
|
|
11277
|
+
const markdownRows = markdownRowsForRender(taskDb, outPath, rows, refRows, workspaceRoot ? { db, workspaceRoot } : {});
|
|
11259
11278
|
const rowsToRender = [...rows, ...markdownRows];
|
|
11260
11279
|
const statusCounts = taskRenderStatusCounts(rowsToRender);
|
|
11261
11280
|
const markdown = taskDb.renderTodoMarkdown(rowsToRender, { doneLimit, failedLimit, refRows, preservedSections });
|
|
11262
11281
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
11263
|
-
|
|
11282
|
+
writeFileAtomic(outPath, markdown);
|
|
11264
11283
|
if (wantsJson(args)) {
|
|
11265
11284
|
printJson({
|
|
11266
11285
|
ok: true,
|
|
@@ -11839,16 +11858,7 @@ function postTaskApiAccept({ res, taskDb, db, taskId, body }) {
|
|
|
11839
11858
|
if (hasExplicitNext && !nextTask.trim()) clearedFields.push('next_task');
|
|
11840
11859
|
const parsedReward = parseAcceptReward(body.reward);
|
|
11841
11860
|
if (!parsedReward.ok) return sendJson(res, 400, { ok: false, reason: 'invalid_reward', detail: 'reward must be a positive number' });
|
|
11842
|
-
const done = taskDb.
|
|
11843
|
-
id: taskId,
|
|
11844
|
-
status: 'done',
|
|
11845
|
-
actor: String(body.actor || DEFAULT_OWNER),
|
|
11846
|
-
allowReview: true,
|
|
11847
|
-
action: 'accepted',
|
|
11848
|
-
proof,
|
|
11849
|
-
});
|
|
11850
|
-
if (!done.updated) return sendJson(res, 409, { ok: false, reason: 'not_open_claimed_or_review' });
|
|
11851
|
-
const reviewed = taskDb.reviewTask(db, {
|
|
11861
|
+
const done = taskDb.acceptTask(db, {
|
|
11852
11862
|
id: taskId,
|
|
11853
11863
|
actor: String(body.actor || DEFAULT_OWNER),
|
|
11854
11864
|
reward: parsedReward.value,
|
|
@@ -11858,6 +11868,8 @@ function postTaskApiAccept({ res, taskDb, db, taskId, body }) {
|
|
|
11858
11868
|
careerXpEligible: true,
|
|
11859
11869
|
clearedFields,
|
|
11860
11870
|
});
|
|
11871
|
+
if (!done.updated) return sendJson(res, 409, { ok: false, reason: 'not_open_claimed_or_review' });
|
|
11872
|
+
const reviewed = done.reviewed;
|
|
11861
11873
|
const nextCreated = body.createNext ? createNextTaskIfRequested(taskDb, db, ['--create-next'], currentTask, reviewed.episode.next_task_suggestion) : null;
|
|
11862
11874
|
const xpProjection = refreshCareerXpAfterReview(reviewed);
|
|
11863
11875
|
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
@@ -12260,6 +12272,7 @@ module.exports = {
|
|
|
12260
12272
|
delegateTask,
|
|
12261
12273
|
AGENT_ENV_MARKERS,
|
|
12262
12274
|
autoRenderTodoFromDb,
|
|
12275
|
+
completeTodoTaskByTitle,
|
|
12263
12276
|
cmdCertifyVerified,
|
|
12264
12277
|
refreshKeptTaskList,
|
|
12265
12278
|
keptCount,
|
package/commands/unknowns.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const { mapNotesPath } = require('../lib/map-refs');
|
|
5
|
+
const { mapNotesPath, mapNotesExcerpt } = require('../lib/map-refs');
|
|
6
6
|
const { spawnSync } = require('child_process');
|
|
7
7
|
const taskDb = require('../lib/task-db');
|
|
8
8
|
const {
|
|
@@ -166,10 +166,14 @@ function formatTaskEvents(rows) {
|
|
|
166
166
|
}).join('\n');
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
function gatherTerritoryContext(root, db) {
|
|
169
|
+
function gatherTerritoryContext(root, db, problem = '') {
|
|
170
170
|
const map = readFileBestEffort(path.join(root, 'atris', 'MAP.md'), { lines: 100 });
|
|
171
171
|
const notesPath = mapNotesPath(root);
|
|
172
|
-
|
|
172
|
+
// Follow the notes index to the area notes that match the problem; with no
|
|
173
|
+
// match, fall back to the top of the notes file.
|
|
174
|
+
const matched = notesPath ? mapNotesExcerpt(root, problem, READ_LIMIT) : '';
|
|
175
|
+
const notes = matched || (notesPath ? readFileBestEffort(path.join(root, notesPath), { lines: 100 }) : '');
|
|
176
|
+
const notesTitle = matched ? 'map notes lines that match the problem' : `${notesPath} first 100 lines`;
|
|
173
177
|
const lessonsMd = readFileBestEffort(path.join(root, 'atris', 'lessons.md'), { maxChars: READ_LIMIT });
|
|
174
178
|
const lessonsJson = readFileBestEffort(path.join(root, 'atris', 'lessons.json'), { maxChars: READ_LIMIT });
|
|
175
179
|
const journals = readRecentJournals(root);
|
|
@@ -179,7 +183,7 @@ function gatherTerritoryContext(root, db) {
|
|
|
179
183
|
return [
|
|
180
184
|
section('workspace_root', root),
|
|
181
185
|
section('atris/MAP.md first 100 lines', map),
|
|
182
|
-
...(notesPath ? [section(
|
|
186
|
+
...(notesPath ? [section(notesTitle, notes)] : []),
|
|
183
187
|
section('atris/lessons.md', lessonsMd),
|
|
184
188
|
section('atris/lessons.json', lessonsJson),
|
|
185
189
|
section('last 3 daily journals', journals),
|
|
@@ -617,7 +621,7 @@ async function unknownsCommand(args = []) {
|
|
|
617
621
|
return 1;
|
|
618
622
|
}
|
|
619
623
|
|
|
620
|
-
const territoryContext = gatherTerritoryContext(root, db);
|
|
624
|
+
const territoryContext = gatherTerritoryContext(root, db, problem);
|
|
621
625
|
const analysis = analyzeWithModel(root, problem, territoryContext);
|
|
622
626
|
const unknowns = normalizeUnknowns(analysis);
|
|
623
627
|
const questions = normalizeQuestions(analysis, problem);
|
package/commands/visualize.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const {
|
|
3
|
+
const { mapNotesExcerpt } = require('../lib/map-refs');
|
|
4
4
|
const { spawnSync } = require('child_process');
|
|
5
5
|
const { getLogPath } = require('../lib/journal');
|
|
6
6
|
const { ensureValidCredentials } = require('../utils/auth');
|
|
@@ -195,17 +195,6 @@ function findRelevantContextFiles(cwd, prompt) {
|
|
|
195
195
|
.slice(0, 4);
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
// The deep map is large: keep only the lines that share a word with the prompt.
|
|
199
|
-
function mapNotesExcerpt(cwd, prompt, maxChars) {
|
|
200
|
-
const notesPath = mapNotesPath(cwd);
|
|
201
|
-
if (!notesPath) return '';
|
|
202
|
-
const words = promptWords(prompt);
|
|
203
|
-
if (!words.size) return '';
|
|
204
|
-
const lines = fs.readFileSync(path.join(cwd, notesPath), 'utf8').split(/\r?\n/)
|
|
205
|
-
.filter(line => [...words].some(word => line.toLowerCase().includes(word)));
|
|
206
|
-
return lines.join('\n').slice(0, maxChars);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
198
|
function collectWorkspaceContext(prompt, cwd = process.cwd()) {
|
|
210
199
|
const business = readBusinessMeta(cwd);
|
|
211
200
|
const chunks = [];
|
package/commands/workflow.js
CHANGED
|
@@ -19,6 +19,7 @@ const {
|
|
|
19
19
|
const { startFirstTalk } = require('../lib/context-gatherer');
|
|
20
20
|
const { isNonInteractive } = require('../lib/noninteractive');
|
|
21
21
|
const { loadContext } = require('../lib/state-detection');
|
|
22
|
+
const { resolveClaudeRunnerModel } = require('../lib/runner-command');
|
|
22
23
|
const { buildToolResultBody } = require('../lib/tool-result-encode');
|
|
23
24
|
const { commitReviewLearning } = require('./learn');
|
|
24
25
|
|
|
@@ -502,7 +503,7 @@ function mapPromptSection(targetDir, cwd = process.cwd()) {
|
|
|
502
503
|
if (!notes) return `## MAP.md\nRead this file for file:line references: ${mapRef}\n\n`;
|
|
503
504
|
const notesRef = path.relative(cwd, path.join(path.dirname(targetDir), notes));
|
|
504
505
|
return `## MAP.md\nRead this file to find where work lives: ${mapRef}\n\n`
|
|
505
|
-
+ `## MAP-NOTES.md\nRead this file for exact file:line references: ${notesRef}\n\n`;
|
|
506
|
+
+ `## MAP-NOTES.md\nRead this file for exact file:line references, or the area note it points to: ${notesRef}\n\n`;
|
|
506
507
|
}
|
|
507
508
|
|
|
508
509
|
async function planAtris(userInput = null) {
|
|
@@ -1741,7 +1742,7 @@ async function reviewAtris() {
|
|
|
1741
1742
|
// Handoff prompt: suggest writing handoff if completions exist today
|
|
1742
1743
|
if (fs.existsSync(logFile)) {
|
|
1743
1744
|
const journalContent = fs.readFileSync(logFile, 'utf8');
|
|
1744
|
-
const hasCompletions = /## Completed ✅[\s\S]*?- \*\*C\d+:/.test(journalContent);
|
|
1745
|
+
const hasCompletions = /## Completed(?: ✅)?[^\S\n]*\n[\s\S]*?- \*\*C\d+:/.test(journalContent);
|
|
1745
1746
|
const hasHandoff = /## Handoff[\s\S]*?\*\*Context:\*\*/.test(journalContent);
|
|
1746
1747
|
|
|
1747
1748
|
if (hasCompletions && !hasHandoff) {
|
|
@@ -1834,7 +1835,7 @@ async function executeAgentSDKFast(userInput) {
|
|
|
1834
1835
|
const postData = JSON.stringify({
|
|
1835
1836
|
message: userInput,
|
|
1836
1837
|
workspace_path: process.cwd(),
|
|
1837
|
-
model:
|
|
1838
|
+
model: resolveClaudeRunnerModel()
|
|
1838
1839
|
});
|
|
1839
1840
|
|
|
1840
1841
|
const options = {
|