atris 3.80.1 → 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/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 +109 -13
- package/commands/release.js +41 -10
- 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/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/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 +2 -2
package/commands/mission.js
CHANGED
|
@@ -17,6 +17,7 @@ const {
|
|
|
17
17
|
} = require('../lib/cli-json');
|
|
18
18
|
const escapeRegExp = require('../lib/escape-regexp');
|
|
19
19
|
const { sleepSync } = require('../lib/sleep-sync');
|
|
20
|
+
const { stealStaleLock } = require('../lib/file-lock');
|
|
20
21
|
const {
|
|
21
22
|
appendBriefRecord,
|
|
22
23
|
stampBriefOutcome,
|
|
@@ -363,7 +364,20 @@ function isLiveInFlightMission(mission, now = Date.now(), root = process.cwd())
|
|
|
363
364
|
if (!mission || !mission.id || mission.cloud === true) return false;
|
|
364
365
|
const status = String(mission.status || '');
|
|
365
366
|
if (TERMINAL_STATUSES.has(status) || status === 'paused' || status === 'blocked') return false;
|
|
366
|
-
if (status === 'running')
|
|
367
|
+
if (status === 'running') {
|
|
368
|
+
// Session-driven missions sit at running between ticks with no process,
|
|
369
|
+
// so a missing driver alone is normal. A recorded driver whose process is
|
|
370
|
+
// gone, plus a day with no tick, is a crashed run: not live.
|
|
371
|
+
const driver = missionDriverHealth(mission, root);
|
|
372
|
+
if (driver.alive === true || !driver.pid) return true;
|
|
373
|
+
const lastActivity = Math.max(
|
|
374
|
+
Date.parse(mission.last_tick_at || '') || 0,
|
|
375
|
+
Date.parse(mission.resumed_at || '') || 0,
|
|
376
|
+
Date.parse(mission.started_at || '') || 0,
|
|
377
|
+
Date.parse(mission.created_at || '') || 0,
|
|
378
|
+
);
|
|
379
|
+
return !lastActivity || now - lastActivity <= LIVE_MISSION_TICK_MS;
|
|
380
|
+
}
|
|
367
381
|
const lastTickAt = Date.parse(mission.last_tick_at || '');
|
|
368
382
|
if (!Number.isFinite(lastTickAt)) return false;
|
|
369
383
|
const age = now - lastTickAt;
|
|
@@ -743,6 +757,48 @@ function resolveMission(ref, root = process.cwd()) {
|
|
|
743
757
|
return null;
|
|
744
758
|
}
|
|
745
759
|
|
|
760
|
+
// Operator halts (stop, pause, complete) land while a long tick or run is in
|
|
761
|
+
// flight. A writer that read the mission before the halt must not save its
|
|
762
|
+
// stale status over it. Returns the latest row when it halted since
|
|
763
|
+
// `baseline` was read, else null.
|
|
764
|
+
const OPERATOR_HALT_STATUSES = new Set(['stopped', 'paused', 'complete']);
|
|
765
|
+
const OPERATOR_HALT_FIELDS = [
|
|
766
|
+
'status', 'stopped_at', 'paused_at', 'completed_at', 'stop_reason',
|
|
767
|
+
'proof', 'completion_gate', 'landing', 'result', 'next_action',
|
|
768
|
+
];
|
|
769
|
+
|
|
770
|
+
function missionHaltedSince(baseline, root = process.cwd()) {
|
|
771
|
+
if (!baseline || !baseline.id) return null;
|
|
772
|
+
const latest = loadMissionMap(root).get(baseline.id);
|
|
773
|
+
if (!latest) return null;
|
|
774
|
+
const status = String(latest.status || '');
|
|
775
|
+
if (!OPERATOR_HALT_STATUSES.has(status) || status === String(baseline.status || '')) return null;
|
|
776
|
+
return latest;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Keep a halt that landed meanwhile: the writer's progress fields survive,
|
|
780
|
+
// the halt's status fields win.
|
|
781
|
+
function keepOperatorHalt(next, halted) {
|
|
782
|
+
if (!halted) return next;
|
|
783
|
+
const merged = { ...next };
|
|
784
|
+
for (const key of OPERATOR_HALT_FIELDS) {
|
|
785
|
+
if (Object.prototype.hasOwnProperty.call(halted, key)) merged[key] = halted[key];
|
|
786
|
+
}
|
|
787
|
+
return merged;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Operator writes (stop, pause, complete) re-read right before saving, so a
|
|
791
|
+
// tick that saved while they ran keeps its progress instead of being rolled
|
|
792
|
+
// back to the copy read at the start of the command.
|
|
793
|
+
function withLatestMissionProgress(mission, root = process.cwd()) {
|
|
794
|
+
if (!mission || !mission.id) return mission;
|
|
795
|
+
const latest = loadMissionMap(root).get(mission.id);
|
|
796
|
+
if (!latest) return mission;
|
|
797
|
+
const latestAt = Date.parse(latest.updated_at || '') || 0;
|
|
798
|
+
const seenAt = Date.parse(mission.updated_at || '') || 0;
|
|
799
|
+
return latestAt > seenAt ? { ...mission, ...latest } : mission;
|
|
800
|
+
}
|
|
801
|
+
|
|
746
802
|
function saveMission(mission, root = process.cwd(), eventType = 'mission_updated', payload = {}) {
|
|
747
803
|
const paths = statePaths(root);
|
|
748
804
|
const existing = mission && mission.id ? loadMissionMap(root).get(mission.id) : null;
|
|
@@ -7610,6 +7666,11 @@ function installDetachedMissionDriverLifecycle(mission, root = process.cwd()) {
|
|
|
7610
7666
|
return { finish, setExitReason };
|
|
7611
7667
|
}
|
|
7612
7668
|
|
|
7669
|
+
function missionHasDeadDriver(mission, root = process.cwd()) {
|
|
7670
|
+
const driver = missionDriverHealth(mission, root);
|
|
7671
|
+
return Boolean(driver.pid) && driver.alive !== true;
|
|
7672
|
+
}
|
|
7673
|
+
|
|
7613
7674
|
function missionDriverHealth(mission, root = process.cwd()) {
|
|
7614
7675
|
const paths = missionDriverPaths(mission, root);
|
|
7615
7676
|
const driverState = readJson(paths.stateFile);
|
|
@@ -7648,6 +7709,10 @@ function acquireMissionLock(missionId, root = process.cwd(), options = {}) {
|
|
|
7648
7709
|
return { ok: true, lockFile, fd, missionId, driverPid: process.pid, startedAt, recordLength: record.length };
|
|
7649
7710
|
} catch (e) {
|
|
7650
7711
|
if (e.code === 'EEXIST') {
|
|
7712
|
+
let judged = null;
|
|
7713
|
+
try { judged = fs.statSync(lockFile); } catch { continue; }
|
|
7714
|
+
let judgedRaw = null;
|
|
7715
|
+
try { judgedRaw = fs.readFileSync(lockFile, 'utf8'); } catch {}
|
|
7651
7716
|
const info = readJson(lockFile, {}) || {};
|
|
7652
7717
|
const holderPid = Number(info.pid);
|
|
7653
7718
|
const holderKnown = Number.isInteger(holderPid) && holderPid > 0;
|
|
@@ -7659,13 +7724,13 @@ function acquireMissionLock(missionId, root = process.cwd(), options = {}) {
|
|
|
7659
7724
|
// creation window (guards against a crash between openSync and writeSync).
|
|
7660
7725
|
let stealable = holderKnown && !missionLockOwnerIsAlive(holderPid);
|
|
7661
7726
|
if (!holderKnown) {
|
|
7662
|
-
|
|
7663
|
-
try { ageMs = Date.now() - fs.statSync(lockFile).mtimeMs; } catch { ageMs = 0; }
|
|
7664
|
-
stealable = ageMs > 1000;
|
|
7727
|
+
stealable = Date.now() - judged.mtimeMs > 1000;
|
|
7665
7728
|
}
|
|
7666
7729
|
if (stealable) {
|
|
7730
|
+
// Rename-then-verify, not unlink: two waiters that both judged the
|
|
7731
|
+
// same dead lock used to unlink each other's fresh lock in turn.
|
|
7667
7732
|
try {
|
|
7668
|
-
|
|
7733
|
+
stealStaleLock(lockFile, judged, judgedRaw);
|
|
7669
7734
|
continue;
|
|
7670
7735
|
} catch {}
|
|
7671
7736
|
}
|
|
@@ -7700,8 +7765,22 @@ function updateMissionLockOwner(lock, pid, extra = {}) {
|
|
|
7700
7765
|
|
|
7701
7766
|
function releaseMissionLock(lock) {
|
|
7702
7767
|
if (!lock || !lock.ok) return;
|
|
7768
|
+
// Unlink only while the path still names the file this lock opened; a
|
|
7769
|
+
// lock stolen as stale now belongs to someone else.
|
|
7770
|
+
let ours = true;
|
|
7771
|
+
if (lock.fd != null) {
|
|
7772
|
+
try {
|
|
7773
|
+
const mine = fs.fstatSync(lock.fd);
|
|
7774
|
+
const current = fs.statSync(lock.lockFile);
|
|
7775
|
+
ours = mine.ino === current.ino && mine.dev === current.dev;
|
|
7776
|
+
} catch {
|
|
7777
|
+
ours = false;
|
|
7778
|
+
}
|
|
7779
|
+
}
|
|
7703
7780
|
try { if (lock.fd != null) fs.closeSync(lock.fd); } catch {}
|
|
7704
|
-
|
|
7781
|
+
if (ours) {
|
|
7782
|
+
try { fs.unlinkSync(lock.lockFile); } catch {}
|
|
7783
|
+
}
|
|
7705
7784
|
}
|
|
7706
7785
|
|
|
7707
7786
|
// Runners that need a live human session cannot be driven headlessly; every
|
|
@@ -9885,7 +9964,9 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9885
9964
|
// demote terminal status back to ready (always-on + verifier-pass used to
|
|
9886
9965
|
// reopen closed rooms, mission 32 complete@19:23:33 → ready@19:23:48).
|
|
9887
9966
|
const latestOnDisk = loadMissionMap(cwd).get(mission.id) || mission;
|
|
9888
|
-
|
|
9967
|
+
// A pause issued during this tick stops the loop too, not just stop/complete.
|
|
9968
|
+
const pausedMeanwhile = String(latestOnDisk.status || '') === 'paused' && String(mission.status || '') !== 'paused';
|
|
9969
|
+
const alreadyTerminal = TERMINAL_STATUSES.has(String(latestOnDisk.status || '')) || pausedMeanwhile;
|
|
9889
9970
|
// A passing verifier alone never completes a mission whose linked task
|
|
9890
9971
|
// is not accepted: park it ready and name the accept command instead.
|
|
9891
9972
|
const acceptGate = (verifierResult?.passed && completeOnPass && !fullBudgetMode && !mission.always_on && !xpReadyAction)
|
|
@@ -10611,8 +10692,11 @@ function tickMission(args) {
|
|
|
10611
10692
|
...(nextGoalChain ? { goal_chain: nextGoalChain } : {}),
|
|
10612
10693
|
next_action: nextAction,
|
|
10613
10694
|
};
|
|
10614
|
-
|
|
10695
|
+
// The tick can run for minutes; a stop/pause/complete issued meanwhile wins.
|
|
10696
|
+
const haltedMeanwhile = missionHaltedSince(mission, cwd);
|
|
10697
|
+
const { mission: saved } = saveMission(keepOperatorHalt(nextMission, haltedMeanwhile), cwd, 'mission_tick', {
|
|
10615
10698
|
tick_index: tickIdx, verify, verifier_result: verifierResult, receipt_path: receiptPath, layer: tickRecord.layer,
|
|
10699
|
+
...(haltedMeanwhile ? { kept_operator_status: haltedMeanwhile.status } : {}),
|
|
10616
10700
|
});
|
|
10617
10701
|
const blocker = selfDrive && saved.status === 'blocked'
|
|
10618
10702
|
? require('../lib/self-drive').handleMissionBlocker({
|
|
@@ -10808,7 +10892,7 @@ function completeMission(args) {
|
|
|
10808
10892
|
const baselineSummary = pruneMissionWorktreeBaseline(mission, root);
|
|
10809
10893
|
const completionGate = { ...gate, forced: force && !gate.ok, ...(force && !gate.ok ? { forced_reason: gate.reason } : {}) };
|
|
10810
10894
|
const baseNext = {
|
|
10811
|
-
...mission,
|
|
10895
|
+
...withLatestMissionProgress(mission, root),
|
|
10812
10896
|
status: 'complete',
|
|
10813
10897
|
completed_at: stampIso(),
|
|
10814
10898
|
proof,
|
|
@@ -10881,7 +10965,7 @@ function stopMissionRecord(mission, reason, root = process.cwd(), options = {})
|
|
|
10881
10965
|
}
|
|
10882
10966
|
const baselineSummary = pause ? null : pruneMissionWorktreeBaseline(mission, root);
|
|
10883
10967
|
const next = {
|
|
10884
|
-
...mission,
|
|
10968
|
+
...withLatestMissionProgress(mission, root),
|
|
10885
10969
|
status,
|
|
10886
10970
|
stopped_at: status === 'stopped' ? stampIso() : mission.stopped_at || null,
|
|
10887
10971
|
paused_at: status === 'paused' ? stampIso() : mission.paused_at || null,
|
|
@@ -10930,8 +11014,9 @@ function stopMission(args) {
|
|
|
10930
11014
|
// A mission parked in paused/planning/ready and untouched for a week is
|
|
10931
11015
|
// abandoned in practice, nobody resumes it, and each one is a line the
|
|
10932
11016
|
// operator re-reads forever. The daily autoland tick expires them to
|
|
10933
|
-
// stopped with a revive hint; running
|
|
10934
|
-
// recently
|
|
11017
|
+
// stopped with a revive hint; a running mission is aged out only when its
|
|
11018
|
+
// recorded driver died, anything touched recently never is, and a tick on an
|
|
11019
|
+
// expired id revives it.
|
|
10935
11020
|
const MISSION_IDLE_EXPIRY_DAYS = 7;
|
|
10936
11021
|
const EXPIRABLE_STATUSES = new Set(['paused', 'planning', 'ready']);
|
|
10937
11022
|
|
|
@@ -10969,7 +11054,14 @@ function expireStaleMissions(root = process.cwd(), { idleDays = MISSION_IDLE_EXP
|
|
|
10969
11054
|
const held = [];
|
|
10970
11055
|
for (const mission of listMissions(root)) {
|
|
10971
11056
|
const status = String(mission.status || '').toLowerCase();
|
|
10972
|
-
|
|
11057
|
+
// A mission left at running by a driver that died is as abandoned as a
|
|
11058
|
+
// parked one. Only the default sweep takes it, and only when a driver was
|
|
11059
|
+
// recorded and its process is gone (session-driven runs have none).
|
|
11060
|
+
const deadRunning = status === 'running'
|
|
11061
|
+
&& statuses === EXPIRABLE_STATUSES
|
|
11062
|
+
&& mission.cloud !== true
|
|
11063
|
+
&& missionHasDeadDriver(mission, root);
|
|
11064
|
+
if (!statuses.has(status) && !deadRunning) continue;
|
|
10973
11065
|
// updated_at and paused_at are machine-polluted, status renders and
|
|
10974
11066
|
// goal controllers re-save parked missions daily, so a mission nobody
|
|
10975
11067
|
// has run since May reads as "touched today". Real activity is the
|
|
@@ -11858,6 +11950,10 @@ function missionCommand(args) {
|
|
|
11858
11950
|
}
|
|
11859
11951
|
|
|
11860
11952
|
module.exports = {
|
|
11953
|
+
isLiveInFlightMission,
|
|
11954
|
+
missionHaltedSince,
|
|
11955
|
+
keepOperatorHalt,
|
|
11956
|
+
withLatestMissionProgress,
|
|
11861
11957
|
receiptLearnedLine,
|
|
11862
11958
|
tickTouchedCheckDefinition,
|
|
11863
11959
|
engineVerifierLineup,
|
package/commands/release.js
CHANGED
|
@@ -12,8 +12,13 @@ const { runGit } = require('../lib/checkout-sync');
|
|
|
12
12
|
* - Commits, tags, pushes
|
|
13
13
|
* - Creates GitHub release via `gh`
|
|
14
14
|
* - Drafts a /launch post (3 emoji bullets)
|
|
15
|
+
*
|
|
16
|
+
* A real release (not --dry-run) runs releasePreflight first, checked against the tag it is
|
|
17
|
+
* about to make: on master, clean, level with origin/master, the tag free, the tests green.
|
|
18
|
+
* Any failure refuses the release before anything is written and returns 1. A push that
|
|
19
|
+
* fails returns 1 too, with the exact commands to finish or undo by hand.
|
|
15
20
|
*/
|
|
16
|
-
async function releaseAtris({ dryRun = false } = {}) {
|
|
21
|
+
async function releaseAtris({ dryRun = false, preflight = releasePreflight } = {}) {
|
|
17
22
|
const cwd = process.cwd();
|
|
18
23
|
|
|
19
24
|
// 1. Get last tag
|
|
@@ -80,7 +85,15 @@ async function releaseAtris({ dryRun = false } = {}) {
|
|
|
80
85
|
return;
|
|
81
86
|
}
|
|
82
87
|
|
|
83
|
-
// 7.
|
|
88
|
+
// 7. Refuse to release from anywhere but a clean master that passes preflight.
|
|
89
|
+
const preflightCode = preflight({ cwd, tagVersion: nextVersion });
|
|
90
|
+
if (preflightCode !== 0) {
|
|
91
|
+
console.log('');
|
|
92
|
+
console.log('release refused: preflight failed, nothing was changed. fix the failures above and run `atris release` again.');
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 8. Let npm keep package.json and package-lock.json aligned.
|
|
84
97
|
execFileSync('npm', [
|
|
85
98
|
'version',
|
|
86
99
|
nextVersion,
|
|
@@ -89,22 +102,38 @@ async function releaseAtris({ dryRun = false } = {}) {
|
|
|
89
102
|
], { cwd, stdio: 'ignore' });
|
|
90
103
|
console.log(`bumped package.json and package-lock.json to ${nextVersion}`);
|
|
91
104
|
|
|
92
|
-
//
|
|
105
|
+
// 9. Commit
|
|
93
106
|
execFileSync('git', ['add', 'package.json', 'package-lock.json'], { cwd });
|
|
94
107
|
execFileSync('git', ['commit', '-m', `v${nextVersion}`], { cwd });
|
|
95
108
|
console.log('committed');
|
|
96
109
|
|
|
97
|
-
//
|
|
110
|
+
// 9b. Tag
|
|
98
111
|
execFileSync('git', ['tag', `v${nextVersion}`], { cwd });
|
|
99
112
|
console.log(`tagged v${nextVersion}`);
|
|
100
113
|
|
|
101
|
-
// 10. Push
|
|
114
|
+
// 10. Push the commit, then the one tag. A failure stops here: a GitHub release made
|
|
115
|
+
// for a tag that never reached origin would create that tag on the wrong commit.
|
|
116
|
+
const tag = `v${nextVersion}`;
|
|
117
|
+
try {
|
|
118
|
+
execFileSync('git', ['push', 'origin', 'master'], { cwd, stdio: 'pipe' });
|
|
119
|
+
} catch (err) {
|
|
120
|
+
// Nothing reached origin. Drop the local tag so a retry is not blocked by it.
|
|
121
|
+
try { execFileSync('git', ['tag', '-d', tag], { cwd, stdio: 'pipe' }); } catch { /* the tag may not exist */ }
|
|
122
|
+
console.log(`push failed: the version commit ${tag} is only on this machine, and its local tag was deleted.`);
|
|
123
|
+
console.log('to finish by hand:');
|
|
124
|
+
console.log(` git push origin master && git tag ${tag} && git push origin ${tag}`);
|
|
125
|
+
console.log('or to undo the version commit:');
|
|
126
|
+
console.log(' git reset --hard HEAD~1');
|
|
127
|
+
return 1;
|
|
128
|
+
}
|
|
102
129
|
try {
|
|
103
|
-
execFileSync('git', ['push'], { cwd });
|
|
104
|
-
execFileSync('git', ['push', '--tags'], { cwd });
|
|
130
|
+
execFileSync('git', ['push', 'origin', tag], { cwd, stdio: 'pipe' });
|
|
105
131
|
console.log('pushed');
|
|
106
132
|
} catch (err) {
|
|
107
|
-
console.log(
|
|
133
|
+
console.log(`push failed: the version commit is on origin/master, but tag ${tag} is only on this machine.`);
|
|
134
|
+
console.log('to finish by hand:');
|
|
135
|
+
console.log(` git push origin ${tag}`);
|
|
136
|
+
return 1;
|
|
108
137
|
}
|
|
109
138
|
|
|
110
139
|
// 11. Create GitHub release via gh
|
|
@@ -210,7 +239,9 @@ function gitStdout(result) {
|
|
|
210
239
|
* each check and exits nonzero if any fail. Skips the test suite when earlier
|
|
211
240
|
* git checks already failed so a bad checkout does not burn a full run.
|
|
212
241
|
*/
|
|
213
|
-
|
|
242
|
+
// tagVersion: the version whose tag must be free. `atris release` passes the version it is about to
|
|
243
|
+
// make; the bare preflight checks the version package.json already carries.
|
|
244
|
+
function releasePreflight({ cwd = process.cwd(), runTests = runFullTestSuite, tagVersion = null } = {}) {
|
|
214
245
|
let failed = false;
|
|
215
246
|
|
|
216
247
|
const branchResult = runGit(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
|
|
@@ -245,7 +276,7 @@ function releasePreflight({ cwd = process.cwd(), runTests = runFullTestSuite } =
|
|
|
245
276
|
console.log('pass: local master matches origin/master');
|
|
246
277
|
}
|
|
247
278
|
|
|
248
|
-
const version = readPackageVersion(cwd);
|
|
279
|
+
const version = tagVersion || readPackageVersion(cwd);
|
|
249
280
|
if (!version) {
|
|
250
281
|
failed = true;
|
|
251
282
|
console.log('fail: package.json version is missing');
|
package/commands/status.js
CHANGED
|
@@ -193,7 +193,7 @@ function statusAtris(isQuick = false, jsonMode = false, verbose = false) {
|
|
|
193
193
|
.filter(Boolean);
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
const completedMatch = logContent.match(/## Completed
|
|
196
|
+
const completedMatch = logContent.match(/## Completed(?: ✅)?[^\S\n]*\n([\s\S]*?)(?=\n##|---|$)/);
|
|
197
197
|
if (completedMatch && completedMatch[1].trim()) {
|
|
198
198
|
completions = completedMatch[1].trim().split('\n')
|
|
199
199
|
.filter(l => l.match(/^- \*\*C\d+:/))
|
package/commands/sync.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
|
+
const { ensureProjectTasteFile } = require('../lib/taste-notes');
|
|
2
3
|
const path = require('path');
|
|
3
4
|
const { stateHome } = require('../lib/state-home');
|
|
4
5
|
const { ensureWikiScaffold } = require('../lib/wiki');
|
|
@@ -178,11 +179,10 @@ function syncPackageSkills(targetAtrisDir, opts = {}) {
|
|
|
178
179
|
fs.symlinkSync(relativePath, symlinkPath);
|
|
179
180
|
if (verbose) console.log(`✓ Linked .claude/skills/${skill}`);
|
|
180
181
|
} catch (e) {
|
|
182
|
+
// No symlinks here (Windows without the right): copy the whole skill,
|
|
183
|
+
// since a skill may link to notes beside its SKILL.md.
|
|
181
184
|
ensureRealDirectory(symlinkPath);
|
|
182
|
-
|
|
183
|
-
if (fs.existsSync(skillFile)) {
|
|
184
|
-
fs.copyFileSync(skillFile, path.join(symlinkPath, 'SKILL.md'));
|
|
185
|
-
}
|
|
185
|
+
fs.cpSync(destSkillDir, symlinkPath, { recursive: true });
|
|
186
186
|
if (verbose) console.log(`✓ Copied .claude/skills/${skill} (symlink failed)`);
|
|
187
187
|
}
|
|
188
188
|
}
|
|
@@ -568,6 +568,11 @@ function syncAtris(options = {}) {
|
|
|
568
568
|
console.log(`${dryRun ? 'Would create' : '✓ Created'} atris/policies/ folder`);
|
|
569
569
|
}
|
|
570
570
|
|
|
571
|
+
// Project taste is the project's own; create a starter only when missing.
|
|
572
|
+
if (ensureProjectTasteFile(targetDir, { dryRun })) {
|
|
573
|
+
console.log(`${dryRun ? 'Would create' : '✓ Created'} atris/taste/TASTE.md`);
|
|
574
|
+
}
|
|
575
|
+
|
|
571
576
|
const filesToSync = [
|
|
572
577
|
{ source: 'atris.md', target: 'atris.md' },
|
|
573
578
|
{ source: 'PERSONA.md', target: 'PERSONA.md' },
|
|
@@ -885,11 +890,10 @@ function syncSkills({ silent = false } = {}) {
|
|
|
885
890
|
}
|
|
886
891
|
} catch (e) {
|
|
887
892
|
// Fallback: copy instead of symlink
|
|
893
|
+
// No symlinks here (Windows without the right): copy the whole skill,
|
|
894
|
+
// since a skill may link to notes beside its SKILL.md.
|
|
888
895
|
ensureRealDirectory(symlinkPath);
|
|
889
|
-
|
|
890
|
-
if (fs.existsSync(skillFile)) {
|
|
891
|
-
fs.copyFileSync(skillFile, path.join(symlinkPath, 'SKILL.md'));
|
|
892
|
-
}
|
|
896
|
+
fs.cpSync(destSkillDir, symlinkPath, { recursive: true });
|
|
893
897
|
}
|
|
894
898
|
}
|
|
895
899
|
}
|