atris 3.80.0 → 3.82.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/LICENSE +21 -0
- package/README.md +10 -0
- 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/auth.js +2 -2
- 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 +166 -2413
- 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 +109 -13
- package/commands/release.js +41 -10
- package/commands/security-review.js +5 -1
- package/commands/site-deploy.js +5 -1
- package/commands/status.js +1 -1
- package/commands/sync.js +12 -8
- package/commands/task.js +144 -118
- package/commands/unknowns.js +9 -5
- package/commands/visualize.js +1 -12
- package/commands/workflow.js +4 -3
- package/commands/worktree.js +68 -1602
- 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/computer-core.js +2447 -0
- package/lib/engine-ask.js +1 -1
- package/lib/engine-receipt-sweep.js +64 -0
- package/lib/file-lock.js +175 -0
- package/lib/fleet.js +1 -1
- package/lib/integrations-core.js +1695 -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-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/recall-exam.js +171 -0
- package/lib/roster-markdown.js +2 -2
- package/lib/runner-command.js +5 -5
- package/lib/security-audit-classes.js +20 -2
- package/lib/security-audit.js +31 -1
- package/lib/task-db.js +105 -37
- 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 +1517 -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 +10 -3
- package/utils/auth.js +18 -4
package/commands/task.js
CHANGED
|
@@ -47,6 +47,7 @@ const {
|
|
|
47
47
|
} = require('../lib/task-explanation');
|
|
48
48
|
const { extractReceiptEvidence, RECEIPT_PATH_PATTERN } = require('../lib/receipt-evidence');
|
|
49
49
|
const escapeRegExp = require('../lib/escape-regexp');
|
|
50
|
+
const { writeFileAtomic } = require('../lib/json-file');
|
|
50
51
|
const reviewIntegrity = require('../lib/review-integrity');
|
|
51
52
|
const { gateForHuman, isRetiredFillerReason, landingWhyClause, numberWord, plainLandingReason } = require('../lib/voice-gate');
|
|
52
53
|
const {
|
|
@@ -184,14 +185,15 @@ function writeDefaultProjection(taskDb, db, options = {}) {
|
|
|
184
185
|
}));
|
|
185
186
|
const outPath = path.resolve(root || process.cwd(), '.atris', 'state', 'tasks.projection.json');
|
|
186
187
|
const output = JSON.stringify(projection, null, 2) + '\n';
|
|
187
|
-
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
188
188
|
let shouldWrite = true;
|
|
189
189
|
try {
|
|
190
190
|
shouldWrite = fs.readFileSync(outPath, 'utf8') !== output;
|
|
191
191
|
} catch {
|
|
192
192
|
shouldWrite = true;
|
|
193
193
|
}
|
|
194
|
-
|
|
194
|
+
// Temp file and rename: a reader never sees half a projection. This is the one writer of the
|
|
195
|
+
// projection's shape; lib/mission-core.js writes through it too.
|
|
196
|
+
if (shouldWrite) writeFileAtomic(outPath, output);
|
|
195
197
|
return { projection, outPath };
|
|
196
198
|
}
|
|
197
199
|
|
|
@@ -4787,16 +4789,13 @@ function cmdReviews(args) {
|
|
|
4787
4789
|
}
|
|
4788
4790
|
|
|
4789
4791
|
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);
|
|
4792
|
+
taskDb.updateTaskMetadata(db, taskId, (metadata) => {
|
|
4793
|
+
metadata.accept_group = group;
|
|
4794
|
+
metadata.accept_group_by = actor;
|
|
4795
|
+
metadata.accept_group_at = new Date().toISOString();
|
|
4796
|
+
metadata.accepted_by_sampling = !verified;
|
|
4797
|
+
if (!verified) metadata.sample_verified_ids = sampleVerifiedIds;
|
|
4798
|
+
});
|
|
4800
4799
|
}
|
|
4801
4800
|
|
|
4802
4801
|
// Accept a whole certified cluster honestly: the human spot-checks K rows (real career XP), the rest
|
|
@@ -4902,16 +4901,7 @@ function cmdAcceptGroup(args) {
|
|
|
4902
4901
|
accepted.push({ id: task.id, ok: false, reason: MISSION_XP_END_TO_END_REASON, detail: missionXpIssue });
|
|
4903
4902
|
continue;
|
|
4904
4903
|
}
|
|
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, {
|
|
4904
|
+
const done = taskDb.acceptTask(db, {
|
|
4915
4905
|
id: task.id,
|
|
4916
4906
|
actor,
|
|
4917
4907
|
reward: 1,
|
|
@@ -4920,6 +4910,8 @@ function cmdAcceptGroup(args) {
|
|
|
4920
4910
|
proof,
|
|
4921
4911
|
careerXpEligible: isVerified, // honest: career XP ONLY for rows the human actually verified
|
|
4922
4912
|
});
|
|
4913
|
+
if (!done.updated) { accepted.push({ id: task.id, ok: false, reason: 'not_review' }); continue; }
|
|
4914
|
+
const reviewed = done.reviewed;
|
|
4923
4915
|
refreshCareerXpAfterReview(reviewed);
|
|
4924
4916
|
stampAcceptGroupMetadata(taskDb, db, task.id, { actor, group: groupLabel, verified: isVerified, sampleVerifiedIds: [...verifiedIds] });
|
|
4925
4917
|
accepted.push({ id: task.id, ok: true, verified: isVerified, career_xp: isVerified });
|
|
@@ -9390,19 +9382,8 @@ async function cmdAccept(args) {
|
|
|
9390
9382
|
console.error('atris task accept: reward must be a positive number');
|
|
9391
9383
|
process.exit(2);
|
|
9392
9384
|
}
|
|
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, {
|
|
9385
|
+
// One transaction: done and accepted land together or not at all (lib/task-db.js acceptTask).
|
|
9386
|
+
const done = taskDb.acceptTask(db, {
|
|
9406
9387
|
id: taskId,
|
|
9407
9388
|
actor,
|
|
9408
9389
|
reward: parsedReward.value,
|
|
@@ -9412,6 +9393,11 @@ async function cmdAccept(args) {
|
|
|
9412
9393
|
careerXpEligible: true,
|
|
9413
9394
|
clearedFields,
|
|
9414
9395
|
});
|
|
9396
|
+
if (!done.updated) {
|
|
9397
|
+
console.error(`accept failed: ${taskId} not open|claimed|review`);
|
|
9398
|
+
process.exit(1);
|
|
9399
|
+
}
|
|
9400
|
+
const reviewed = done.reviewed;
|
|
9415
9401
|
const xpProjection = refreshCareerXpAfterReview(reviewed);
|
|
9416
9402
|
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
9417
9403
|
const workspaceRoot = projection.workspace_root || process.cwd();
|
|
@@ -9485,17 +9471,13 @@ function stampAutoAcceptMetadata(taskDb, db, task, actor, policy) {
|
|
|
9485
9471
|
} catch (err) {
|
|
9486
9472
|
landing = { status: 'unverified', reason: 'landing_proof_failed', detail: String((err && err.message) || err).slice(0, 200) };
|
|
9487
9473
|
}
|
|
9488
|
-
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
|
|
9493
|
-
|
|
9494
|
-
|
|
9495
|
-
SET metadata = ?,
|
|
9496
|
-
updated_at = ?
|
|
9497
|
-
WHERE id = ?
|
|
9498
|
-
`).run(JSON.stringify(metadata), Date.now(), taskId);
|
|
9474
|
+
// The landing proof runs git, so it is worked out first; only the write holds the lock.
|
|
9475
|
+
taskDb.updateTaskMetadata(db, taskId, (metadata) => {
|
|
9476
|
+
metadata.auto_accepted_at = new Date().toISOString();
|
|
9477
|
+
metadata.auto_accepted_by = actor;
|
|
9478
|
+
metadata.auto_accept_policy = policy;
|
|
9479
|
+
metadata.landing = { ...landing, checked_at: metadata.auto_accepted_at };
|
|
9480
|
+
});
|
|
9499
9481
|
return landing;
|
|
9500
9482
|
}
|
|
9501
9483
|
|
|
@@ -9512,19 +9494,8 @@ function acceptReviewTask(taskDb, db, taskId, { actor, proof, reward, lesson = '
|
|
|
9512
9494
|
if (missionXpIssue) {
|
|
9513
9495
|
return { ok: false, reason: MISSION_XP_END_TO_END_REASON, detail: missionXpIssue };
|
|
9514
9496
|
}
|
|
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, {
|
|
9497
|
+
// One transaction: done and accepted land together or not at all (lib/task-db.js acceptTask).
|
|
9498
|
+
const accepted = taskDb.acceptTask(db, {
|
|
9528
9499
|
id: taskId,
|
|
9529
9500
|
actor,
|
|
9530
9501
|
reward,
|
|
@@ -9534,6 +9505,10 @@ function acceptReviewTask(taskDb, db, taskId, { actor, proof, reward, lesson = '
|
|
|
9534
9505
|
careerXpEligible: true,
|
|
9535
9506
|
autoAccepted,
|
|
9536
9507
|
});
|
|
9508
|
+
if (!accepted.updated) {
|
|
9509
|
+
return { ok: false, reason: 'not_open_claimed_or_review' };
|
|
9510
|
+
}
|
|
9511
|
+
const reviewed = accepted.reviewed;
|
|
9537
9512
|
const acceptedRow = taskDb.getTask(db, taskId);
|
|
9538
9513
|
refreshExistingTodoMarkdown(taskDb, db, acceptedRow && acceptedRow.workspace_root);
|
|
9539
9514
|
return { ok: true, reviewed };
|
|
@@ -9580,19 +9555,12 @@ function verifyIsOnlyFileChecks(verify) {
|
|
|
9580
9555
|
}
|
|
9581
9556
|
|
|
9582
9557
|
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);
|
|
9558
|
+
taskDb.updateTaskMetadata(db, taskId, (metadata) => {
|
|
9559
|
+
metadata.verify = metadata.verify || verify;
|
|
9560
|
+
metadata.certified_verified_at = new Date().toISOString();
|
|
9561
|
+
metadata.certified_verified_by = actor;
|
|
9562
|
+
metadata.machine_verified = true;
|
|
9563
|
+
});
|
|
9596
9564
|
}
|
|
9597
9565
|
|
|
9598
9566
|
function currentHeadSha(workspaceRoot) {
|
|
@@ -9613,27 +9581,23 @@ function currentHeadSha(workspaceRoot) {
|
|
|
9613
9581
|
// certify-verified and the autoland landing re-check, persist the outcome
|
|
9614
9582
|
// here for the read path to consult.
|
|
9615
9583
|
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 = {
|
|
9584
|
+
if (!taskDb.getTask(db, taskId)) return;
|
|
9585
|
+
const verifyCache = {
|
|
9620
9586
|
command: verify,
|
|
9621
9587
|
ok: result && result.ok === true,
|
|
9622
9588
|
reason: (result && result.reason) || null,
|
|
9623
9589
|
ran_at: new Date().toISOString(),
|
|
9624
9590
|
ran_by: actor,
|
|
9625
9591
|
// The commit the check ran on: the job's own code when it has some,
|
|
9626
|
-
// else the folder the job lives in.
|
|
9592
|
+
// else the folder the job lives in. Read before the write lock is taken.
|
|
9627
9593
|
code_sha: (result && result.code_sha) || currentHeadSha(workspaceRoot),
|
|
9628
9594
|
code_source: (result && result.code_source) || 'checkout',
|
|
9629
9595
|
};
|
|
9630
9596
|
// A check is not a touch. Bumping updated_at here put every failed job at
|
|
9631
9597
|
// 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);
|
|
9598
|
+
taskDb.updateTaskMetadata(db, taskId, (metadata) => {
|
|
9599
|
+
metadata.verify_cache = verifyCache;
|
|
9600
|
+
}, { touch: false });
|
|
9637
9601
|
}
|
|
9638
9602
|
|
|
9639
9603
|
// When a review job was last given a second check; never checked reads 0.
|
|
@@ -9656,23 +9620,16 @@ function failedOnSameCode(task, verify, codeSha) {
|
|
|
9656
9620
|
// metadata.verify is what lets sweep --auto-accept (and certify-verified)
|
|
9657
9621
|
// re-run it later against the current checkout instead of re-parsing prose.
|
|
9658
9622
|
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);
|
|
9623
|
+
taskDb.updateTaskMetadata(db, taskId, (metadata) => {
|
|
9624
|
+
metadata.verify = verify;
|
|
9625
|
+
// A real verify stamped at ready means the creation-time degraded flag is
|
|
9626
|
+
// stale: clear it so `task show` stops warning about a missing check. A
|
|
9627
|
+
// diff-only verify keeps the flag; it still cannot fail on real breakage.
|
|
9628
|
+
if (verify.toLowerCase() !== 'git diff --check') {
|
|
9629
|
+
delete metadata.verification_status;
|
|
9630
|
+
delete metadata.verification_degraded_reason;
|
|
9631
|
+
}
|
|
9632
|
+
});
|
|
9676
9633
|
}
|
|
9677
9634
|
|
|
9678
9635
|
function landingVerifyFailureNote(verify, result) {
|
|
@@ -11095,13 +11052,29 @@ function renderedTaskRefSet(taskDb, rows, refRows) {
|
|
|
11095
11052
|
return refs;
|
|
11096
11053
|
}
|
|
11097
11054
|
|
|
11098
|
-
|
|
11055
|
+
// A bullet the renderer wrote starts with its job's ref in bold brackets: `- **[WSX-1]** title`.
|
|
11056
|
+
const RENDERED_TODO_BULLET_REF = /^\s*- (?:\[[ x]\]\s+)?\*\*\[([^\]]+)\]\*\*/;
|
|
11057
|
+
|
|
11058
|
+
// Rows in TODO.md that the database does not know. In a hand-written TODO they are rendered
|
|
11059
|
+
// from the markdown, as before. In a rendered TODO (it has the "Regenerated" marker) a bullet
|
|
11060
|
+
// the renderer wrote whose job is gone stays gone, but a bullet a person or a command added by
|
|
11061
|
+
// hand (no bracketed ref, like `atris business` writing `- **Onboard:** ...`) is imported into
|
|
11062
|
+
// the database when `db` is given, so the next render keeps it instead of dropping it. Imported
|
|
11063
|
+
// rows are pushed onto `rows` and `refRows`, so the caller renders them with the rest.
|
|
11064
|
+
function markdownRowsForRender(taskDb, existingTodoPath, rows, refRows, { db = null, workspaceRoot: wsOption = null } = {}) {
|
|
11099
11065
|
if (!existingTodoPath || !fs.existsSync(existingTodoPath)) return [];
|
|
11100
11066
|
const { parseTodoFile } = require('../lib/todo-fallback');
|
|
11101
11067
|
const existingTodo = fs.readFileSync(existingTodoPath, 'utf8');
|
|
11102
11068
|
const generatedTodo = existingTodo.includes('Regenerated from durable Atris task state');
|
|
11103
11069
|
const parsed = parseTodoFile(existingTodoPath);
|
|
11104
|
-
const ws = taskDb.workspaceRoot();
|
|
11070
|
+
const ws = wsOption || taskDb.workspaceRoot();
|
|
11071
|
+
const renderedBulletRefs = new Set();
|
|
11072
|
+
if (generatedTodo) {
|
|
11073
|
+
for (const line of existingTodo.split('\n')) {
|
|
11074
|
+
const match = line.match(RENDERED_TODO_BULLET_REF);
|
|
11075
|
+
if (match) renderedBulletRefs.add(normalizeRenderedTaskRef(match[1]));
|
|
11076
|
+
}
|
|
11077
|
+
}
|
|
11105
11078
|
const existingRefs = renderedTaskRefSet(taskDb, rows, refRows);
|
|
11106
11079
|
const existingSourceKeys = new Set(
|
|
11107
11080
|
(Array.isArray(refRows) ? refRows : [])
|
|
@@ -11130,9 +11103,38 @@ function markdownRowsForRender(taskDb, existingTodoPath, rows, refRows) {
|
|
|
11130
11103
|
if (
|
|
11131
11104
|
(renderedRef && existingRefs.has(renderedRef)) ||
|
|
11132
11105
|
(sk && existingSourceKeys.has(sk)) ||
|
|
11133
|
-
existingTitles.has(normalizedTitle)
|
|
11134
|
-
generatedTodo
|
|
11106
|
+
existingTitles.has(normalizedTitle)
|
|
11135
11107
|
) continue;
|
|
11108
|
+
if (generatedTodo) {
|
|
11109
|
+
// Written by the renderer and no longer in the database: leave it out.
|
|
11110
|
+
// Only open and in-progress bullets are imported: a hand-written line under Review or
|
|
11111
|
+
// Completed has no history to stand on.
|
|
11112
|
+
if (!db || (renderedRef && renderedBulletRefs.has(renderedRef)) || (status !== 'open' && status !== 'claimed')) continue;
|
|
11113
|
+
const claimer = status === 'claimed' ? (task.claimed || null) : null;
|
|
11114
|
+
const added = taskDb.addTask(db, {
|
|
11115
|
+
title: task.title,
|
|
11116
|
+
tag: task.tag || null,
|
|
11117
|
+
workspaceRoot: ws,
|
|
11118
|
+
sourceKey: sk,
|
|
11119
|
+
status: status === 'claimed' && !claimer ? 'open' : status,
|
|
11120
|
+
claimedBy: claimer,
|
|
11121
|
+
metadata: {
|
|
11122
|
+
source: 'todo_markdown_import',
|
|
11123
|
+
todo_id: task.id || null,
|
|
11124
|
+
todo_tags: task.tags || [],
|
|
11125
|
+
...(task.verify ? { verify: task.verify } : {}),
|
|
11126
|
+
},
|
|
11127
|
+
});
|
|
11128
|
+
const imported = added && added.id ? taskDb.getTask(db, added.id) : null;
|
|
11129
|
+
if (imported) {
|
|
11130
|
+
if (Array.isArray(rows)) rows.push(imported);
|
|
11131
|
+
if (Array.isArray(refRows) && refRows !== rows) refRows.push(imported);
|
|
11132
|
+
existingRefs.add(normalizeRenderedTaskRef(imported.id));
|
|
11133
|
+
}
|
|
11134
|
+
if (sk) existingSourceKeys.add(sk);
|
|
11135
|
+
existingTitles.add(normalizedTitle);
|
|
11136
|
+
continue;
|
|
11137
|
+
}
|
|
11136
11138
|
out.push({
|
|
11137
11139
|
id: `markdown:${status}:${task.id || index}:${sk ? sk.slice(0, 10) : index}`,
|
|
11138
11140
|
title: task.title,
|
|
@@ -11204,12 +11206,12 @@ function refreshExistingTodoMarkdown(taskDb, db, workspaceRoot) {
|
|
|
11204
11206
|
const preservedSections = [];
|
|
11205
11207
|
const endgameSection = extractTodoSectionMarkdown(existingTodo, 'Endgame');
|
|
11206
11208
|
if (endgameSection) preservedSections.push(endgameSection);
|
|
11207
|
-
const markdownRows = markdownRowsForRender(taskDb, outPath, rows, refRows);
|
|
11209
|
+
const markdownRows = markdownRowsForRender(taskDb, outPath, rows, refRows, { db, workspaceRoot: ws });
|
|
11208
11210
|
const markdown = taskDb.renderTodoMarkdown([...rows, ...markdownRows], {
|
|
11209
11211
|
refRows,
|
|
11210
11212
|
preservedSections,
|
|
11211
11213
|
});
|
|
11212
|
-
|
|
11214
|
+
writeFileAtomic(outPath, markdown);
|
|
11213
11215
|
return outPath;
|
|
11214
11216
|
}
|
|
11215
11217
|
|
|
@@ -11225,13 +11227,43 @@ function autoRenderTodoFromDb(cwd = process.cwd()) {
|
|
|
11225
11227
|
if (fs.existsSync(outPath)) return refreshExistingTodoMarkdown(taskDb, db, cwd);
|
|
11226
11228
|
const rows = taskDb.listTasks(db, { workspaceRoot: cwd, limit: 500 });
|
|
11227
11229
|
const refRows = taskDb.listTasks(db, { workspaceRoot: cwd });
|
|
11228
|
-
|
|
11230
|
+
writeFileAtomic(outPath, taskDb.renderTodoMarkdown(rows, { refRows }));
|
|
11229
11231
|
return outPath;
|
|
11230
11232
|
} catch {
|
|
11231
11233
|
return null;
|
|
11232
11234
|
}
|
|
11233
11235
|
}
|
|
11234
11236
|
|
|
11237
|
+
// The task behind a TODO bullet, finished the way `atris task done` finishes one: doneTask, then the
|
|
11238
|
+
// projection and the rendered TODO. A rendered TODO.md is a view of the database, so marking its bullet
|
|
11239
|
+
// means nothing until the job itself is done. `matches(title)` says whether a job's title is the one the
|
|
11240
|
+
// caller named; only an open or claimed job in this workspace counts, an exact title wins over a partial
|
|
11241
|
+
// one, and two partial matches are a refusal, not a guess. Returns { completed, id, reason }.
|
|
11242
|
+
function completeTodoTaskByTitle(cwd, title, { matches, actor, proof } = {}) {
|
|
11243
|
+
const taskDb = getTaskDb();
|
|
11244
|
+
const db = taskDb.open();
|
|
11245
|
+
const ws = taskDb.workspaceRoot(cwd);
|
|
11246
|
+
const wanted = taskDb.normalizeTitle(title);
|
|
11247
|
+
const candidates = taskDb.listTasks(db, { workspaceRoot: ws, limit: null })
|
|
11248
|
+
.filter((row) => row.status === 'open' || row.status === 'claimed');
|
|
11249
|
+
const exact = candidates.filter((row) => wanted && taskDb.normalizeTitle(row.title) === wanted);
|
|
11250
|
+
const partial = typeof matches === 'function' ? candidates.filter((row) => matches(row.title)) : [];
|
|
11251
|
+
const picked = exact.length === 1 ? exact : (exact.length ? [] : partial);
|
|
11252
|
+
if (picked.length !== 1) return { completed: false, id: null, reason: picked.length ? 'ambiguous' : 'no_match' };
|
|
11253
|
+
const id = picked[0].id;
|
|
11254
|
+
const done = taskDb.doneTask(db, {
|
|
11255
|
+
id,
|
|
11256
|
+
status: 'done',
|
|
11257
|
+
actor: actor || process.env.ATRIS_AGENT_ID || DEFAULT_OWNER,
|
|
11258
|
+
action: 'done',
|
|
11259
|
+
proof,
|
|
11260
|
+
});
|
|
11261
|
+
if (!done.updated) return { completed: false, id, reason: 'not_open_or_claimed' };
|
|
11262
|
+
try { writeDefaultProjection(taskDb, db, { root: cwd }); } catch { /* the view is rebuilt on the next write */ }
|
|
11263
|
+
autoRenderTodoFromDb(cwd);
|
|
11264
|
+
return { completed: true, id, reason: null };
|
|
11265
|
+
}
|
|
11266
|
+
|
|
11235
11267
|
function cmdRender(args) {
|
|
11236
11268
|
const out = flag(args, '--out') || path.join('atris', 'TODO.md');
|
|
11237
11269
|
const all = hasFlag(args, '--all');
|
|
@@ -11255,12 +11287,12 @@ function cmdRender(args) {
|
|
|
11255
11287
|
const preservedSections = [];
|
|
11256
11288
|
const endgameSection = extractTodoSectionMarkdown(existingTodo, 'Endgame');
|
|
11257
11289
|
if (endgameSection) preservedSections.push(endgameSection);
|
|
11258
|
-
const markdownRows = markdownRowsForRender(taskDb, outPath, rows, refRows);
|
|
11290
|
+
const markdownRows = markdownRowsForRender(taskDb, outPath, rows, refRows, workspaceRoot ? { db, workspaceRoot } : {});
|
|
11259
11291
|
const rowsToRender = [...rows, ...markdownRows];
|
|
11260
11292
|
const statusCounts = taskRenderStatusCounts(rowsToRender);
|
|
11261
11293
|
const markdown = taskDb.renderTodoMarkdown(rowsToRender, { doneLimit, failedLimit, refRows, preservedSections });
|
|
11262
11294
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
11263
|
-
|
|
11295
|
+
writeFileAtomic(outPath, markdown);
|
|
11264
11296
|
if (wantsJson(args)) {
|
|
11265
11297
|
printJson({
|
|
11266
11298
|
ok: true,
|
|
@@ -11839,16 +11871,7 @@ function postTaskApiAccept({ res, taskDb, db, taskId, body }) {
|
|
|
11839
11871
|
if (hasExplicitNext && !nextTask.trim()) clearedFields.push('next_task');
|
|
11840
11872
|
const parsedReward = parseAcceptReward(body.reward);
|
|
11841
11873
|
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, {
|
|
11874
|
+
const done = taskDb.acceptTask(db, {
|
|
11852
11875
|
id: taskId,
|
|
11853
11876
|
actor: String(body.actor || DEFAULT_OWNER),
|
|
11854
11877
|
reward: parsedReward.value,
|
|
@@ -11858,6 +11881,8 @@ function postTaskApiAccept({ res, taskDb, db, taskId, body }) {
|
|
|
11858
11881
|
careerXpEligible: true,
|
|
11859
11882
|
clearedFields,
|
|
11860
11883
|
});
|
|
11884
|
+
if (!done.updated) return sendJson(res, 409, { ok: false, reason: 'not_open_claimed_or_review' });
|
|
11885
|
+
const reviewed = done.reviewed;
|
|
11861
11886
|
const nextCreated = body.createNext ? createNextTaskIfRequested(taskDb, db, ['--create-next'], currentTask, reviewed.episode.next_task_suggestion) : null;
|
|
11862
11887
|
const xpProjection = refreshCareerXpAfterReview(reviewed);
|
|
11863
11888
|
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
@@ -12260,6 +12285,7 @@ module.exports = {
|
|
|
12260
12285
|
delegateTask,
|
|
12261
12286
|
AGENT_ENV_MARKERS,
|
|
12262
12287
|
autoRenderTodoFromDb,
|
|
12288
|
+
completeTodoTaskByTitle,
|
|
12263
12289
|
cmdCertifyVerified,
|
|
12264
12290
|
refreshKeptTaskList,
|
|
12265
12291
|
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 = {
|