atris 3.57.3 → 3.58.5
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/README.md +12 -1
- package/atris/skills/clawhub/member-runtime/SKILL.md +9 -9
- package/atris/skills/create-member/SKILL.md +16 -13
- package/atris/skills/design/SKILL.md +27 -4
- package/atris/skills/engines/SKILL.md +57 -13
- package/atris/skills/x-search/SKILL.md +13 -5
- package/atris/skills/youtube/SKILL.md +39 -8
- package/atris/team/brainstormer/MEMBER.md +2 -2
- package/atris/team/executor/MEMBER.md +3 -3
- package/atris/team/launcher/MEMBER.md +4 -4
- package/atris/team/navigator/MEMBER.md +3 -3
- package/atris/team/researcher/MEMBER.md +2 -2
- package/atris/team/validator/MEMBER.md +1 -1
- package/atris.md +14 -0
- package/ax +157 -24
- package/bin/atris.js +79 -31
- package/commands/aeo.js +4 -4
- package/commands/agents.js +1 -1
- package/commands/align.js +12 -12
- package/commands/analytics.js +3 -3
- package/commands/app.js +5 -5
- package/commands/ask.js +175 -0
- package/commands/auth.js +7 -7
- package/commands/autoland.js +29 -5
- package/commands/autopilot-front.js +6 -6
- package/commands/autopilot.js +90 -90
- package/commands/avail.js +5 -5
- package/commands/bench.js +63 -1
- package/commands/browse.js +2 -2
- package/commands/business.js +12 -12
- package/commands/card.js +2 -2
- package/commands/clean.js +13 -13
- package/commands/close.js +297 -15
- package/commands/compile.js +13 -13
- package/commands/computer.js +94 -27
- package/commands/console.js +5 -5
- package/commands/decide.js +1 -1
- package/commands/deck.js +9 -9
- package/commands/drive.js +4 -4
- package/commands/engine.js +173 -19
- package/commands/errors.js +3 -3
- package/commands/experiments.js +18 -1
- package/commands/feed.js +2 -2
- package/commands/feedback.js +4 -4
- package/commands/fleet.js +12 -12
- package/commands/game.js +1 -1
- package/commands/guide.js +49 -0
- package/commands/improve.js +19 -17
- package/commands/init.js +67 -19
- package/commands/interview.js +7 -7
- package/commands/land.js +40 -15
- package/commands/learn.js +12 -12
- package/commands/lesson.js +2 -2
- package/commands/log.js +1 -1
- package/commands/loops.js +4 -4
- package/commands/member.js +75 -50
- package/commands/mission.js +97 -98
- package/commands/one-lap.js +1 -1
- package/commands/pack.js +74 -8
- package/commands/plugin.js +6 -6
- package/commands/probe.js +9 -9
- package/commands/pull.js +35 -35
- package/commands/pulse.js +1 -1
- package/commands/push.js +21 -21
- package/commands/rainmaker.js +1 -1
- package/commands/recap.js +7 -7
- package/commands/reel.js +2 -2
- package/commands/release.js +3 -3
- package/commands/report.js +1 -1
- package/commands/review.js +5 -5
- package/commands/revisions.js +1 -1
- package/commands/run.js +1 -1
- package/commands/scout.js +3 -3
- package/commands/security-review.js +7 -7
- package/commands/serve.js +9 -9
- package/commands/setup.js +1 -1
- package/commands/sign.js +4 -4
- package/commands/signup.js +4 -4
- package/commands/site.js +8 -3
- package/commands/skill.js +6 -6
- package/commands/slop.js +18 -18
- package/commands/social.js +3 -3
- package/commands/soul.js +14 -14
- package/commands/spaceship.js +1 -1
- package/commands/status.js +2 -2
- package/commands/strings.js +13 -13
- package/commands/study.js +4 -4
- package/commands/sync.js +8 -8
- package/commands/task.js +50 -25
- package/commands/teach.js +2 -2
- package/commands/team.js +1 -1
- package/commands/theme.js +2 -2
- package/commands/tree.js +34 -0
- package/commands/truth.js +7 -7
- package/commands/verify.js +12 -12
- package/commands/visualize.js +2 -2
- package/commands/workflow.js +97 -40
- package/commands/worktree.js +57 -1
- package/commands/write.js +9 -9
- package/commands/x-search.js +304 -15
- package/commands/xp.js +6 -3
- package/commands/youtube.js +916 -65
- package/lib/accept-verify-gate.js +3 -3
- package/lib/activity-stream.js +1 -1
- package/lib/apply-gate.js +13 -0
- package/lib/auto-accept-certified.js +28 -12
- package/lib/autoland.js +6 -6
- package/lib/bench/engines.js +59 -8
- package/lib/bench/report.js +1 -1
- package/lib/bench/runner.js +204 -5
- package/lib/bench/tree-render.js +76 -0
- package/lib/card.js +1 -1
- package/lib/chat-log-scan.js +1 -1
- package/lib/claude-boot-block.js +2 -0
- package/lib/deck-compose.js +1 -1
- package/lib/deck-history.js +2 -2
- package/lib/deck-layout.js +1 -1
- package/lib/deck-review.js +5 -5
- package/lib/deck-schema.js +1 -1
- package/lib/default-verifier.js +1 -1
- package/lib/developer-api.js +3 -0
- package/lib/engine-ask.js +66 -9
- package/lib/engine-registry.js +15 -2
- package/lib/file-ops.js +1 -1
- package/lib/fleet.js +71 -35
- package/lib/html-render.js +1 -1
- package/lib/intents.js +172 -0
- package/lib/journal.js +1 -1
- package/lib/known-commands.js +2 -2
- package/lib/land-green.js +364 -0
- package/lib/lesson-contradiction.js +2 -2
- package/lib/lesson-preflight.js +1 -1
- package/lib/manifest.js +5 -5
- package/lib/member-alive.js +9 -5
- package/lib/memory-view.js +3 -3
- package/lib/mission-protected-lane.js +1 -1
- package/lib/mission-root.js +2 -2
- package/lib/orb-context.js +10 -10
- package/lib/pack-capabilities.js +861 -3
- package/lib/permission-grants.js +2 -2
- package/lib/policy-lessons.js +6 -6
- package/lib/pulse.js +8 -8
- package/lib/receipt-evidence.js +1 -1
- package/lib/reel.js +1 -1
- package/lib/review-integrity.js +1 -1
- package/lib/runner-command.js +35 -5
- package/lib/scorecard.js +4 -4
- package/lib/security-scan.js +5 -5
- package/lib/self-drive.js +1 -1
- package/lib/site-publish.js +372 -0
- package/lib/site.js +1 -1
- package/lib/slides-deck.js +2 -2
- package/lib/state-detection.js +2 -2
- package/lib/sync-telemetry.js +2 -2
- package/lib/task-db.js +17 -10
- package/lib/task-explanation.js +1 -1
- package/lib/task-proof.js +2 -2
- package/lib/task-receipt.js +3 -3
- package/lib/theme.js +12 -12
- package/lib/todo-fallback.js +3 -3
- package/lib/todo-sections.js +1 -1
- package/lib/tree-hash.js +79 -0
- package/lib/usage.js +1 -1
- package/lib/wish-delegate.js +1 -1
- package/lib/workspace-safety.js +4 -4
- package/lib/workspace-scaffold.js +3 -3
- package/package.json +1 -1
- package/scripts/det/concierge-bakeoff.sh +26 -0
- package/scripts/det/ytquote-repair.js +0 -1
- package/scripts/det/ytrail-race.js +0 -5
- package/scripts/member-operate.mjs +52 -14
- package/scripts/outbound-artifact-gate.js +0 -3
- package/utils/auth.js +0 -3
- package/utils/config.js +0 -1
- package/scripts/det/checklist-score.js +0 -191
package/lib/intents.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const INTENTS = Object.freeze([
|
|
4
|
+
Object.freeze({
|
|
5
|
+
id: 'next_work',
|
|
6
|
+
say: Object.freeze(['what should i do next', 'what is next', 'pick the next thing', 'what do we do now']),
|
|
7
|
+
do: 'atris next',
|
|
8
|
+
plain: 'your agent picks the most useful next step and starts it.',
|
|
9
|
+
offer: 'boot_empty',
|
|
10
|
+
}),
|
|
11
|
+
Object.freeze({
|
|
12
|
+
id: 'keep_going',
|
|
13
|
+
say: Object.freeze(['keep going while i\'m away', 'run overnight', 'finish this', 'keep working without me']),
|
|
14
|
+
do: 'atris spaceship',
|
|
15
|
+
plain: 'your agent keeps working for a few hours and sends updates.',
|
|
16
|
+
offer: 'boot_empty',
|
|
17
|
+
confirm: true,
|
|
18
|
+
}),
|
|
19
|
+
Object.freeze({
|
|
20
|
+
id: 'improve_one_thing',
|
|
21
|
+
say: Object.freeze(['make it better', 'make it faster', 'ship one thing', 'improve this', 'find one useful improvement']),
|
|
22
|
+
do: 'atris improve tick',
|
|
23
|
+
plain: 'your agent finds one useful improvement, makes it, and checks the result.',
|
|
24
|
+
offer: 'after_landing',
|
|
25
|
+
}),
|
|
26
|
+
Object.freeze({
|
|
27
|
+
id: 'clean_up',
|
|
28
|
+
say: Object.freeze(['clean this up', 'it is messy', 'organize this project', 'tidy the loose ends']),
|
|
29
|
+
do: 'atris clean',
|
|
30
|
+
plain: 'your agent finds stale or messy project files and cleans up the safe ones.',
|
|
31
|
+
offer: 'after_landing',
|
|
32
|
+
}),
|
|
33
|
+
Object.freeze({
|
|
34
|
+
id: 'find_or_explain',
|
|
35
|
+
say: Object.freeze(['where is x', 'how does x work', 'where does this live', 'show me where this lives', 'explain this code']),
|
|
36
|
+
do: 'read atris/MAP.md',
|
|
37
|
+
plain: 'your agent reads the project map and points you to the exact files.',
|
|
38
|
+
offer: 'always',
|
|
39
|
+
file: true,
|
|
40
|
+
}),
|
|
41
|
+
Object.freeze({
|
|
42
|
+
id: 'remember_this',
|
|
43
|
+
say: Object.freeze(['remember this', 'save this', 'do not lose this', 'keep this for later']),
|
|
44
|
+
do: 'atris later "<their words>"',
|
|
45
|
+
plain: 'your agent saves this as a note for later.',
|
|
46
|
+
offer: 'always',
|
|
47
|
+
}),
|
|
48
|
+
Object.freeze({
|
|
49
|
+
id: 'schedule_work',
|
|
50
|
+
say: Object.freeze(['every day', 'every morning', 'on a schedule', 'run this regularly']),
|
|
51
|
+
do: 'atris loop',
|
|
52
|
+
plain: 'your agent sets up repeated work on the schedule you describe.',
|
|
53
|
+
offer: 'always',
|
|
54
|
+
confirm: true,
|
|
55
|
+
}),
|
|
56
|
+
Object.freeze({
|
|
57
|
+
id: 'build_an_idea',
|
|
58
|
+
say: Object.freeze(['i want to build x', 'here is an idea', 'can we make x', 'turn this idea into something']),
|
|
59
|
+
do: 'atris wish "<their words>"',
|
|
60
|
+
plain: 'your agent turns the idea into a clear job and starts once any gaps are answered.',
|
|
61
|
+
offer: 'always',
|
|
62
|
+
}),
|
|
63
|
+
Object.freeze({
|
|
64
|
+
id: 'catch_up',
|
|
65
|
+
say: Object.freeze(['catch me up', 'what happened', 'what landed', 'show me recent progress']),
|
|
66
|
+
do: 'atris recap',
|
|
67
|
+
plain: 'your agent summarizes what changed and what still needs attention.',
|
|
68
|
+
offer: 'always',
|
|
69
|
+
}),
|
|
70
|
+
Object.freeze({
|
|
71
|
+
id: 'check_the_work',
|
|
72
|
+
say: Object.freeze(['is this done', 'is it safe', 'check it', 'review this work', 'can i trust this']),
|
|
73
|
+
do: 'atris review',
|
|
74
|
+
plain: 'your agent checks the work, names any risks, and reports whether it is ready.',
|
|
75
|
+
offer: 'after_landing',
|
|
76
|
+
}),
|
|
77
|
+
Object.freeze({
|
|
78
|
+
id: 'use_fewer_credits',
|
|
79
|
+
say: Object.freeze(['use a free model', 'use a cheaper model', 'do not burn credits', 'save credits', 'switch to the fast model']),
|
|
80
|
+
do: 'atris engine atris-fast',
|
|
81
|
+
plain: 'your agent switches this project to the included fast model to spend fewer credits.',
|
|
82
|
+
offer: 'always',
|
|
83
|
+
}),
|
|
84
|
+
Object.freeze({
|
|
85
|
+
id: 'hand_off_work',
|
|
86
|
+
say: Object.freeze(['hand this off', 'have someone else do it', 'delegate this', 'give this to a teammate']),
|
|
87
|
+
do: 'atris task delegate "<their words>" --to <member>',
|
|
88
|
+
plain: 'your agent gives the work to the best available teammate and keeps the handoff visible.',
|
|
89
|
+
offer: 'always',
|
|
90
|
+
confirm: true,
|
|
91
|
+
}),
|
|
92
|
+
Object.freeze({
|
|
93
|
+
id: 'show_or_set_up_team',
|
|
94
|
+
say: Object.freeze(['who owns this', 'set up a team', 'who is responsible', 'show me the team']),
|
|
95
|
+
do: 'atris team',
|
|
96
|
+
plain: 'your agent shows who is responsible and helps fill any missing role.',
|
|
97
|
+
offer: 'always',
|
|
98
|
+
}),
|
|
99
|
+
Object.freeze({
|
|
100
|
+
id: 'speak_plainly',
|
|
101
|
+
say: Object.freeze(['talk plainer', 'too much jargon', 'use plain english', 'keep it simple']),
|
|
102
|
+
do: 'atris clarity --set voice=plain',
|
|
103
|
+
plain: 'your agent saves your preference for short, plain replies.',
|
|
104
|
+
offer: 'always',
|
|
105
|
+
}),
|
|
106
|
+
Object.freeze({
|
|
107
|
+
id: 'set_goal',
|
|
108
|
+
say: Object.freeze(['our goal is x', 'we are trying to x', 'set the goal', 'keep everyone focused on x']),
|
|
109
|
+
do: 'atris goal set "<their words>"',
|
|
110
|
+
plain: 'your agent saves the goal so future work stays pointed at it.',
|
|
111
|
+
offer: 'always',
|
|
112
|
+
}),
|
|
113
|
+
Object.freeze({
|
|
114
|
+
id: 'show_capabilities',
|
|
115
|
+
say: Object.freeze(['what can you do', 'how can you help', 'show me my options', 'what should i ask for']),
|
|
116
|
+
do: 'atris guide',
|
|
117
|
+
plain: 'your agent shows the things it can handle in words you already use.',
|
|
118
|
+
offer: 'always',
|
|
119
|
+
}),
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
const STOP_WORDS = new Set([
|
|
123
|
+
'a', 'an', 'and', 'can', 'could', 'for', 'i', 'im', 'is', 'it', 'me', 'my',
|
|
124
|
+
'of', 'on', 'please', 'the', 'this', 'to', 'we', 'you', 'your',
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
function normalizedTokens(text) {
|
|
128
|
+
const words = String(text || '')
|
|
129
|
+
.toLowerCase()
|
|
130
|
+
.replace(/[\u2018\u2019']/g, '')
|
|
131
|
+
.match(/[a-z0-9]+/g) || [];
|
|
132
|
+
return new Set(words.filter((word) => !STOP_WORDS.has(word)));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function tokenOverlapScore(left, right) {
|
|
136
|
+
if (!left.size || !right.size) return 0;
|
|
137
|
+
let overlap = 0;
|
|
138
|
+
for (const token of left) {
|
|
139
|
+
if (right.has(token)) overlap += 1;
|
|
140
|
+
}
|
|
141
|
+
if (!overlap) return 0;
|
|
142
|
+
return (2 * overlap) / (left.size + right.size);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function intentScore(tokens, intent) {
|
|
146
|
+
return intent.say.reduce((best, phrase) => (
|
|
147
|
+
Math.max(best, tokenOverlapScore(tokens, normalizedTokens(phrase)))
|
|
148
|
+
), 0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function matchIntent(text) {
|
|
152
|
+
const tokens = normalizedTokens(text);
|
|
153
|
+
const ranked = INTENTS
|
|
154
|
+
.map((intent, index) => ({ intent, score: intentScore(tokens, intent), index }))
|
|
155
|
+
.sort((left, right) => right.score - left.score || left.index - right.index);
|
|
156
|
+
|
|
157
|
+
// A 0.46 F1 threshold needs about half of the meaningful words to overlap.
|
|
158
|
+
// It catches natural additions such as "please make this faster" while
|
|
159
|
+
// refusing requests that share only one generic word with a phrase.
|
|
160
|
+
const threshold = 0.46;
|
|
161
|
+
if (!ranked.length || ranked[0].score < threshold) {
|
|
162
|
+
return { intent: null, alternatives: ranked.slice(0, 3).map((item) => item.intent) };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
intent: ranked[0].intent,
|
|
167
|
+
score: Number(ranked[0].score.toFixed(3)),
|
|
168
|
+
alternatives: ranked.slice(1, 3).map((item) => item.intent),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = { INTENTS, matchIntent };
|
package/lib/journal.js
CHANGED
|
@@ -84,7 +84,7 @@ function mergeSections(localSections, remoteSections, knownRemoteHash) {
|
|
|
84
84
|
|
|
85
85
|
// Check if the full remote document matches the known state.
|
|
86
86
|
// knownRemoteHash is a hash of the entire document at last sync.
|
|
87
|
-
// If it matches, remote hasn't changed
|
|
87
|
+
// If it matches, remote hasn't changed, prefer all local edits.
|
|
88
88
|
const remoteReconstructed = reconstructJournal(remoteSections);
|
|
89
89
|
const currentRemoteHash = computeContentHash(remoteReconstructed);
|
|
90
90
|
const remoteUnchanged = knownRemoteHash && currentRemoteHash === knownRemoteHash;
|
package/lib/known-commands.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const knownCommands = ['init', 'doctor', 'install', 'workspace', 'log', 'logs', 'later', 'wish', 'ask', 'approve', 'stop', 'ready', 'check', 'drill', 'dream', 'now', 'goal', 'wtf', 'founder', 'orb', 'radar', 'who', 'stream', 'ctop', 'launchpad', 'status', 'analytics', 'revisions', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
|
|
3
|
+
const knownCommands = ['init', 'guide', 'doctor', 'install', 'workspace', 'log', 'logs', 'later', 'wish', 'ask', 'approve', 'stop', 'ready', 'check', 'drill', 'dream', 'now', 'goal', 'wtf', 'founder', 'orb', 'radar', 'who', 'stream', 'ctop', 'launchpad', 'status', 'analytics', 'revisions', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
|
|
4
4
|
'activate', '_activate', 'agent', 'team', 'chat', 'fast', 'ax', 'console', 'serve', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
|
|
5
|
-
'clean', 'close', 'harvest', 'verify', 'recover', 'search', 'scout', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'taste', 'teach', 'plugin', 'experiments', 'bench', 'router', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'watch', 'cloud', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube', 'x-search',
|
|
5
|
+
'clean', 'close', 'harvest', 'verify', 'recover', 'search', 'scout', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'taste', 'teach', 'plugin', 'experiments', 'bench', 'router', 'tree', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'watch', 'cloud', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube', 'x-search',
|
|
6
6
|
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'decide', 'agents', 'probe', 'worktree', 'land', 'caretaker', 'autoland', 'drive', 'aeo', 'slop', 'voice', 'strings', 'write', 'security-review', 'secure', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'game', 'x', 'recap', 'report', 'signup', 'clarity', 'interview', 'meet', 'moves', 'unknowns', 'avail', 'sync-checkout',
|
|
7
7
|
'ci', 'github', 'vercel', 'supabase', 'linear', 'stripe', 'balance', 'usage', 'api-key', 'topup', 'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
|
|
8
8
|
'fork', 'browse', 'publish', 'pack', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet', 'fleet-report', 'loops', 'self-improve', 'compile', 'spaceship', 'truth', 'sign', 'engine', 'engines', 'playbook', 'feed', 'brief', 'social', 'people', 'follow', 'unfollow', 'friends', 'msg', 'inbox', 'invite', 'join'];
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Green landing for stale branches.
|
|
4
|
+
//
|
|
5
|
+
// The landing board already classifies every branch: landed, active, due.
|
|
6
|
+
// Before this module the loop had exactly two moves for finished work that
|
|
7
|
+
// nobody merged: carry it in the digest, then salvage-reap it after the TTL.
|
|
8
|
+
// Both of the branches that sat "in the air" on 2026-09-01 were green and
|
|
9
|
+
// merged cleanly; they waited four days for a human to run the suite by hand.
|
|
10
|
+
//
|
|
11
|
+
// This is the third move. A stale active branch that merges without conflict,
|
|
12
|
+
// touches no protected surface, and passes the full verify command in its own
|
|
13
|
+
// scratch checkout is fast-forwarded onto the base branch and pushed. A branch
|
|
14
|
+
// that fails opens one close flag naming the failing checks so the owner sees
|
|
15
|
+
// it the same day instead of on reap day. Everything else is left alone with
|
|
16
|
+
// a named reason. One branch per tick: the suite is minutes, not seconds.
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const { spawnSync } = require('child_process');
|
|
22
|
+
const { runGit } = require('./git-spawn');
|
|
23
|
+
|
|
24
|
+
const DEFAULT_VERIFY = 'npm test';
|
|
25
|
+
const DEFAULT_VERIFY_TIMEOUT_MS = 20 * 60 * 1000;
|
|
26
|
+
const FAILING_LINE = /^(?:✖|not ok)\s+(.+?)(?:\s+\(\d+(?:\.\d+)?ms\))?\s*$/;
|
|
27
|
+
|
|
28
|
+
function gitOut(args, cwd) {
|
|
29
|
+
const result = runGit(args, { cwd, check: false });
|
|
30
|
+
return { ok: result.status === 0, out: String(result.stdout || '').trim(), err: String(result.stderr || '').trim() };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function branchAuthor(root, branch) {
|
|
34
|
+
const result = gitOut(['log', '-1', '--format=%an', branch], root);
|
|
35
|
+
return result.ok && result.out ? result.out : 'operator';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function mergeConflicts(root, base, branch) {
|
|
39
|
+
// --write-tree exits 1 on conflict, 0 on clean, >1 on error (git >= 2.38).
|
|
40
|
+
const result = runGit(['merge-tree', '--write-tree', '--name-only', base, branch], { cwd: root, check: false });
|
|
41
|
+
if (result.status === 0) return { ok: true, conflicts: [] };
|
|
42
|
+
if (result.status === 1) {
|
|
43
|
+
// Output: tree id, conflicted paths, a blank line, then messages.
|
|
44
|
+
const block = String(result.stdout || '').split(/\r?\n\r?\n/)[0] || '';
|
|
45
|
+
const lines = block.split(/\r?\n/).filter(Boolean);
|
|
46
|
+
return { ok: true, conflicts: lines.slice(1) };
|
|
47
|
+
}
|
|
48
|
+
return { ok: false, error: String(result.stderr || result.stdout || 'merge-tree failed').trim() };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function protectedSurfaces(root, base, branch) {
|
|
52
|
+
let matcher = null;
|
|
53
|
+
try {
|
|
54
|
+
({ matchProtectedMissionDiff: matcher } = require('./mission-protected-lane'));
|
|
55
|
+
} catch {
|
|
56
|
+
return { protected: false, surfaces: [] };
|
|
57
|
+
}
|
|
58
|
+
const diff = runGit(['diff', `${base}...${branch}`], { cwd: root, check: false, maxBuffer: 64 * 1024 * 1024 });
|
|
59
|
+
if (diff.status !== 0) return { protected: true, surfaces: ['unreadable diff'] };
|
|
60
|
+
const verdict = matcher(String(diff.stdout || ''));
|
|
61
|
+
return { protected: Boolean(verdict.protected), surfaces: verdict.surfaces || [] };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function dirtyCheckouts(root, branch) {
|
|
65
|
+
let listWorktrees;
|
|
66
|
+
let statusCounts;
|
|
67
|
+
try {
|
|
68
|
+
({ listWorktrees, statusCounts } = require('../commands/worktree'));
|
|
69
|
+
} catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
const dirty = [];
|
|
73
|
+
for (const wt of listWorktrees(root)) {
|
|
74
|
+
const name = String(wt.branch || '').replace(/^refs\/heads\//, '');
|
|
75
|
+
if (name !== branch) continue;
|
|
76
|
+
const counts = statusCounts(wt.path) || { staged: 0, unstaged: 0, untracked: 0 };
|
|
77
|
+
if (counts.staged + counts.unstaged + counts.untracked > 0) dirty.push(wt.path);
|
|
78
|
+
}
|
|
79
|
+
return dirty;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function failingLines(text, limit = 8) {
|
|
83
|
+
const seen = [];
|
|
84
|
+
for (const raw of String(text || '').split(/\r?\n/)) {
|
|
85
|
+
const match = raw.trim().match(FAILING_LINE);
|
|
86
|
+
if (!match) continue;
|
|
87
|
+
const name = match[1].trim();
|
|
88
|
+
if (/^failing tests:?$/i.test(name)) continue;
|
|
89
|
+
if (!seen.includes(name)) seen.push(name);
|
|
90
|
+
if (seen.length >= limit) break;
|
|
91
|
+
}
|
|
92
|
+
return seen;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function runVerify(command, cwd, { timeoutMs = DEFAULT_VERIFY_TIMEOUT_MS, env = process.env } = {}) {
|
|
96
|
+
const started = Date.now();
|
|
97
|
+
const result = spawnSync(command, {
|
|
98
|
+
cwd,
|
|
99
|
+
shell: true,
|
|
100
|
+
encoding: 'utf8',
|
|
101
|
+
timeout: timeoutMs,
|
|
102
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
103
|
+
env: { ...env, ATRIS_SKIP_UPDATE_CHECK: '1' },
|
|
104
|
+
});
|
|
105
|
+
const output = `${result.stdout || ''}\n${result.stderr || ''}`;
|
|
106
|
+
return {
|
|
107
|
+
command,
|
|
108
|
+
status: result.status,
|
|
109
|
+
timed_out: Boolean(result.error && result.error.code === 'ETIMEDOUT'),
|
|
110
|
+
duration_ms: Date.now() - started,
|
|
111
|
+
failing: failingLines(output),
|
|
112
|
+
tail: output.trim().split(/\r?\n/).slice(-12).join('\n'),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function baseCheckoutState(root, base) {
|
|
117
|
+
const head = gitOut(['symbolic-ref', '--quiet', '--short', 'HEAD'], root);
|
|
118
|
+
const onBase = head.ok && head.out === base;
|
|
119
|
+
if (!onBase) return { onBase: false, dirty: false };
|
|
120
|
+
const status = gitOut(['status', '--porcelain', '--untracked-files=no'], root);
|
|
121
|
+
return { onBase: true, dirty: status.ok && status.out.length > 0 };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Move the base ref to `sha`, respecting a checked-out base worktree: when
|
|
125
|
+
// the root has base checked out and is clean, a fast-forward merge updates
|
|
126
|
+
// both the ref and the files; otherwise the ref moves alone.
|
|
127
|
+
function advanceBase(root, base, sha) {
|
|
128
|
+
const hasLocal = gitOut(['rev-parse', '--verify', '--quiet', `refs/heads/${base}`], root);
|
|
129
|
+
if (!hasLocal.ok) return { ok: true, skipped: 'no_local_branch' };
|
|
130
|
+
const state = baseCheckoutState(root, base);
|
|
131
|
+
if (state.onBase && state.dirty) return { ok: false, reason: 'base_checkout_dirty' };
|
|
132
|
+
if (state.onBase) {
|
|
133
|
+
const merged = gitOut(['merge', '--ff-only', sha], root);
|
|
134
|
+
if (!merged.ok) return { ok: false, reason: 'fast_forward_failed', detail: merged.err || merged.out };
|
|
135
|
+
return { ok: true };
|
|
136
|
+
}
|
|
137
|
+
const before = gitOut(['rev-parse', base], root);
|
|
138
|
+
const moved = gitOut(['update-ref', `refs/heads/${base}`, sha, before.ok ? before.out : ''], root);
|
|
139
|
+
if (!moved.ok) return { ok: false, reason: 'update_ref_failed', detail: moved.err || moved.out };
|
|
140
|
+
return { ok: true };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function pushBase(root, base, sha) {
|
|
144
|
+
const remote = gitOut(['remote', 'get-url', 'origin'], root);
|
|
145
|
+
if (!remote.ok) return { attempted: false, ok: false, reason: 'no_remote' };
|
|
146
|
+
const pushed = runGit(['push', 'origin', `${sha}:refs/heads/${base}`], { cwd: root, check: false });
|
|
147
|
+
return { attempted: true, ok: pushed.status === 0, detail: String(pushed.stderr || pushed.stdout || '').trim().slice(0, 300) };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// The board names the base as origin/master when a remote exists, so the
|
|
151
|
+
// merge target is the remote tip by construction. Refresh it first, and hold
|
|
152
|
+
// when the local branch carries commits origin lacks: pushing would fail and
|
|
153
|
+
// fast-forwarding the local branch would rewrite someone's unpushed work.
|
|
154
|
+
function localBaseName(base) {
|
|
155
|
+
return String(base).replace(/^origin\//, '');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function resolveMergeTarget(root, base) {
|
|
159
|
+
const local = localBaseName(base);
|
|
160
|
+
const remote = gitOut(['remote', 'get-url', 'origin'], root);
|
|
161
|
+
if (!remote.ok) return { ok: true, target: local, refreshed: false };
|
|
162
|
+
spawnSync('git', ['fetch', 'origin', local], { cwd: root, encoding: 'utf8', timeout: 30000 });
|
|
163
|
+
const remoteRef = `refs/remotes/origin/${local}`;
|
|
164
|
+
const exists = gitOut(['rev-parse', '--verify', '--quiet', remoteRef], root);
|
|
165
|
+
if (!exists.ok) return { ok: true, target: local, refreshed: true };
|
|
166
|
+
const hasLocal = gitOut(['rev-parse', '--verify', '--quiet', `refs/heads/${local}`], root);
|
|
167
|
+
if (hasLocal.ok) {
|
|
168
|
+
const counts = gitOut(['rev-list', '--left-right', '--count', `refs/heads/${local}...${remoteRef}`], root);
|
|
169
|
+
const [localOnly, remoteOnly] = counts.ok ? counts.out.split(/\s+/).map((n) => Number(n) || 0) : [0, 0];
|
|
170
|
+
if (localOnly > 0) {
|
|
171
|
+
return { ok: false, reason: 'base_diverged', detail: `local ${local} has ${localOnly} commit(s) origin lacks${remoteOnly ? ` and origin has ${remoteOnly} local lacks` : ''}` };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return { ok: true, target: remoteRef, refreshed: true };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function scratchCheckout(root, branch) {
|
|
178
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-land-green-'));
|
|
179
|
+
const added = gitOut(['worktree', 'add', '--detach', dir, branch], root);
|
|
180
|
+
if (!added.ok) {
|
|
181
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
182
|
+
return { ok: false, error: added.err || added.out };
|
|
183
|
+
}
|
|
184
|
+
return { ok: true, dir };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function dropScratch(root, dir) {
|
|
188
|
+
if (!dir) return;
|
|
189
|
+
runGit(['worktree', 'remove', '--force', dir], { cwd: root, check: false });
|
|
190
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
191
|
+
runGit(['worktree', 'prune'], { cwd: root, check: false });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function installDeps(dir) {
|
|
195
|
+
// Zero-dependency repos verify without install; only run it when a lockfile
|
|
196
|
+
// exists so the scratch checkout can resolve dev tooling.
|
|
197
|
+
if (!fs.existsSync(path.join(dir, 'package-lock.json'))) return { ran: false, ok: true };
|
|
198
|
+
const result = spawnSync('npm', ['ci', '--ignore-scripts', '--no-audit', '--no-fund'], {
|
|
199
|
+
cwd: dir, encoding: 'utf8', timeout: 5 * 60 * 1000, maxBuffer: 16 * 1024 * 1024,
|
|
200
|
+
});
|
|
201
|
+
return { ran: true, ok: result.status === 0, detail: String(result.stderr || '').trim().slice(0, 300) };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function verdictFor(branch, base, extra) {
|
|
205
|
+
return { branch, base, ...extra };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Try to land exactly one branch. Returns a verdict with `action` in:
|
|
209
|
+
// landed, red, skipped, error
|
|
210
|
+
function landOneBranch(root, base, branch, {
|
|
211
|
+
verifyCommand = DEFAULT_VERIFY,
|
|
212
|
+
verifyTimeoutMs = DEFAULT_VERIFY_TIMEOUT_MS,
|
|
213
|
+
dryRun = false,
|
|
214
|
+
push = true,
|
|
215
|
+
runVerifyFn = runVerify,
|
|
216
|
+
env = process.env,
|
|
217
|
+
} = {}) {
|
|
218
|
+
const dirty = dirtyCheckouts(root, branch);
|
|
219
|
+
if (dirty.length) {
|
|
220
|
+
return verdictFor(branch, base, { action: 'skipped', reason: 'checkout_dirty', detail: dirty.join(', ') });
|
|
221
|
+
}
|
|
222
|
+
const conflicts = mergeConflicts(root, base, branch);
|
|
223
|
+
if (!conflicts.ok) return verdictFor(branch, base, { action: 'error', reason: 'merge_check_failed', detail: conflicts.error });
|
|
224
|
+
if (conflicts.conflicts.length) {
|
|
225
|
+
return verdictFor(branch, base, { action: 'skipped', reason: 'merge_conflict', detail: conflicts.conflicts.slice(0, 6).join(', ') });
|
|
226
|
+
}
|
|
227
|
+
const lane = protectedSurfaces(root, base, branch);
|
|
228
|
+
if (lane.protected) {
|
|
229
|
+
return verdictFor(branch, base, { action: 'skipped', reason: 'protected_surface', detail: lane.surfaces.join(', ') });
|
|
230
|
+
}
|
|
231
|
+
const target = resolveMergeTarget(root, base);
|
|
232
|
+
if (!target.ok) return verdictFor(branch, base, { action: 'skipped', reason: target.reason, detail: target.detail });
|
|
233
|
+
if (dryRun) return verdictFor(branch, base, { action: 'skipped', reason: 'dry_run', target: target.target });
|
|
234
|
+
|
|
235
|
+
const scratch = scratchCheckout(root, branch);
|
|
236
|
+
if (!scratch.ok) return verdictFor(branch, base, { action: 'error', reason: 'scratch_checkout_failed', detail: scratch.error });
|
|
237
|
+
try {
|
|
238
|
+
// Verify the merged state, not the branch tip: base may have moved since.
|
|
239
|
+
const merged = gitOut(['merge', '--no-edit', '--no-ff', target.target, '-m', `land: ${branch} onto ${base} after a green verify`], scratch.dir);
|
|
240
|
+
if (!merged.ok) return verdictFor(branch, base, { action: 'error', reason: 'merge_failed', detail: merged.err || merged.out });
|
|
241
|
+
const deps = installDeps(scratch.dir);
|
|
242
|
+
if (!deps.ok) return verdictFor(branch, base, { action: 'error', reason: 'install_failed', detail: deps.detail });
|
|
243
|
+
const verify = runVerifyFn(verifyCommand, scratch.dir, { timeoutMs: verifyTimeoutMs, env });
|
|
244
|
+
if (verify.status !== 0) {
|
|
245
|
+
return verdictFor(branch, base, {
|
|
246
|
+
action: 'red',
|
|
247
|
+
reason: verify.timed_out ? 'verify_timed_out' : 'verify_failed',
|
|
248
|
+
verify,
|
|
249
|
+
owner: branchAuthor(root, branch),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
const sha = gitOut(['rev-parse', 'HEAD'], scratch.dir);
|
|
253
|
+
if (!sha.ok) return verdictFor(branch, base, { action: 'error', reason: 'merged_sha_unreadable' });
|
|
254
|
+
const local = localBaseName(base);
|
|
255
|
+
const pushed = push ? pushBase(root, local, sha.out) : { attempted: false, ok: false, reason: 'push_disabled' };
|
|
256
|
+
if (pushed.attempted && !pushed.ok) {
|
|
257
|
+
// A race with another landing: origin moved after the fetch. Leave the
|
|
258
|
+
// local branch alone so the next tick retries onto the new tip.
|
|
259
|
+
return verdictFor(branch, base, { action: 'skipped', reason: 'push_rejected', detail: pushed.detail, verify });
|
|
260
|
+
}
|
|
261
|
+
const advanced = advanceBase(root, local, sha.out);
|
|
262
|
+
if (!advanced.ok) return verdictFor(branch, base, { action: 'landed', sha: sha.out, verify, push: pushed, local_base: advanced.reason });
|
|
263
|
+
return verdictFor(branch, base, { action: 'landed', sha: sha.out, verify, push: pushed });
|
|
264
|
+
} finally {
|
|
265
|
+
dropScratch(root, scratch.dir);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function flagWhat(verdict) {
|
|
270
|
+
const names = (verdict.verify && verdict.verify.failing) || [];
|
|
271
|
+
const head = names.length ? names.slice(0, 3).join('; ') : (verdict.reason === 'verify_timed_out' ? 'the verify timed out' : 'the verify command failed');
|
|
272
|
+
return `branch ${verdict.branch} fails its checks and cannot land: ${head}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function openRedFlag(root, verdict, { runCli } = {}) {
|
|
276
|
+
const what = flagWhat(verdict);
|
|
277
|
+
const args = ['close', 'add', what, '--owner', verdict.owner || 'operator', '--lane', 'code', '--ttl', '3',
|
|
278
|
+
'--when', `${verdict.branch} passes ${verdict.verify ? verdict.verify.command : 'its checks'} and lands on ${verdict.base}`,
|
|
279
|
+
'--source', 'land-green'];
|
|
280
|
+
if (typeof runCli === 'function') return runCli(args);
|
|
281
|
+
const bin = path.join(__dirname, '..', 'bin', 'atris.js');
|
|
282
|
+
const result = spawnSync(process.execPath, [bin, ...args], { cwd: root, encoding: 'utf8', timeout: 30000 });
|
|
283
|
+
return { status: result.status, stdout: String(result.stdout || '').trim(), stderr: String(result.stderr || '').trim() };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// The tick entry point: pick the stale active branches the board already
|
|
287
|
+
// knows about, oldest activity first, and land at most `limit` of them.
|
|
288
|
+
function landStaleGreenBranches(root, {
|
|
289
|
+
board = null,
|
|
290
|
+
staleHours,
|
|
291
|
+
ttlDays,
|
|
292
|
+
base: baseOverride = '',
|
|
293
|
+
limit = 1,
|
|
294
|
+
now = Date.now(),
|
|
295
|
+
verifyCommand = DEFAULT_VERIFY,
|
|
296
|
+
verifyTimeoutMs = DEFAULT_VERIFY_TIMEOUT_MS,
|
|
297
|
+
dryRun = false,
|
|
298
|
+
push = true,
|
|
299
|
+
openFlags = true,
|
|
300
|
+
runVerifyFn = runVerify,
|
|
301
|
+
runCli,
|
|
302
|
+
env = process.env,
|
|
303
|
+
} = {}) {
|
|
304
|
+
const { collectBoard } = require('../commands/land');
|
|
305
|
+
const live = board || collectBoard(root, { staleHours, ttlDays, base: baseOverride, now, light: true });
|
|
306
|
+
const candidates = live.branches
|
|
307
|
+
.filter((b) => b.state === 'active' && b.stale)
|
|
308
|
+
.sort((a, b) => b.activityHours - a.activityHours);
|
|
309
|
+
const verdicts = [];
|
|
310
|
+
let attempts = 0;
|
|
311
|
+
for (const candidate of candidates) {
|
|
312
|
+
if (attempts >= limit) {
|
|
313
|
+
verdicts.push(verdictFor(candidate.name, live.base, { action: 'skipped', reason: 'per_tick_limit' }));
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
attempts += 1;
|
|
317
|
+
let verdict;
|
|
318
|
+
try {
|
|
319
|
+
verdict = landOneBranch(root, live.base, candidate.name, { verifyCommand, verifyTimeoutMs, dryRun, push, runVerifyFn, env });
|
|
320
|
+
} catch (err) {
|
|
321
|
+
verdict = verdictFor(candidate.name, live.base, { action: 'error', reason: 'exception', detail: String((err && err.message) || err).slice(0, 300) });
|
|
322
|
+
}
|
|
323
|
+
if (verdict.action === 'red' && openFlags) {
|
|
324
|
+
try {
|
|
325
|
+
const flagged = openRedFlag(root, verdict, { runCli });
|
|
326
|
+
verdict.flag = { status: flagged.status, out: String(flagged.stdout || '').slice(0, 200) };
|
|
327
|
+
} catch (err) {
|
|
328
|
+
verdict.flag = { status: 1, out: String((err && err.message) || err).slice(0, 200) };
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
verdicts.push(verdict);
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
at: new Date(now).toISOString(),
|
|
335
|
+
base: live.base,
|
|
336
|
+
candidates: candidates.map((b) => b.name),
|
|
337
|
+
landed: verdicts.filter((v) => v.action === 'landed').map((v) => v.branch),
|
|
338
|
+
red: verdicts.filter((v) => v.action === 'red').map((v) => v.branch),
|
|
339
|
+
verdicts,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function summaryLine(result) {
|
|
344
|
+
if (!result || !result.candidates || result.candidates.length === 0) return 'no stale branches to land';
|
|
345
|
+
const parts = [];
|
|
346
|
+
if (result.landed.length) parts.push(`landed ${result.landed.join(', ')}`);
|
|
347
|
+
if (result.red.length) parts.push(`red: ${result.red.join(', ')}`);
|
|
348
|
+
for (const v of result.verdicts) {
|
|
349
|
+
if (v.action === 'skipped' && v.reason !== 'per_tick_limit') parts.push(`${v.branch} held (${v.reason.replace(/_/g, ' ')}${v.detail ? `: ${v.detail}` : ''})`);
|
|
350
|
+
if (v.action === 'error') parts.push(`${v.branch} error (${v.reason.replace(/_/g, ' ')})`);
|
|
351
|
+
}
|
|
352
|
+
const waiting = result.verdicts.filter((v) => v.reason === 'per_tick_limit').length;
|
|
353
|
+
if (waiting) parts.push(`${waiting} more waiting for the next tick`);
|
|
354
|
+
return parts.join('; ') || 'nothing landed';
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
module.exports = {
|
|
358
|
+
DEFAULT_VERIFY,
|
|
359
|
+
landStaleGreenBranches,
|
|
360
|
+
|
|
361
|
+
failingLines,
|
|
362
|
+
flagWhat,
|
|
363
|
+
summaryLine,
|
|
364
|
+
};
|
|
@@ -22,8 +22,8 @@ function detectLessonContradictions(cwd) {
|
|
|
22
22
|
? fs.readFileSync(lessonsMdPath, 'utf8')
|
|
23
23
|
: '';
|
|
24
24
|
|
|
25
|
-
// Format: - **[YYYY-MM-DD] slug
|
|
26
|
-
const lessonRegex = /^-\s+\*\*\[(\d{4}-\d{2}-\d{2})\]\s+([a-z0-9-]+)\*\*\s
|
|
25
|
+
// Format: - **[YYYY-MM-DD] slug**, pass|fail, prose
|
|
26
|
+
const lessonRegex = /^-\s+\*\*\[(\d{4}-\d{2}-\d{2})\]\s+([a-z0-9-]+)\*\*\s*\u2014\s*(pass|fail)\s*\u2014\s*(\[resolved\])?\s*(.+)$/m;
|
|
27
27
|
const lines = lessonsMdContent.split('\n');
|
|
28
28
|
|
|
29
29
|
const lessonsBySlug = new Map(); // slug -> [{ date, outcome, lineNum, resolved }]
|
package/lib/lesson-preflight.js
CHANGED
|
@@ -28,7 +28,7 @@ function readLessonText(filePath) {
|
|
|
28
28
|
|
|
29
29
|
const bySlug = new Map();
|
|
30
30
|
for (const line of content.split(/\r?\n/)) {
|
|
31
|
-
const match = line.match(/^\s*-\s*\*\*\[([^\]]+)\]\s*([a-z0-9][a-z0-9-]*)\*\*\s*[
|
|
31
|
+
const match = line.match(/^\s*-\s*\*\*\[([^\]]+)\]\s*([a-z0-9][a-z0-9-]*)\*\*\s*[\u2014:-]+\s*(?:pass|fail)?\s*[\u2014:-]*\s*(.*)$/i);
|
|
32
32
|
if (!match || bySlug.has(match[2])) continue;
|
|
33
33
|
const text = match[3].replace(/^\[resolved\]\s*/i, '').trim();
|
|
34
34
|
if (text) bySlug.set(match[2], text);
|
package/lib/manifest.js
CHANGED
|
@@ -152,14 +152,14 @@ function threeWayCompare(localFiles, remoteFiles, manifest) {
|
|
|
152
152
|
newRemote: [],
|
|
153
153
|
};
|
|
154
154
|
|
|
155
|
-
// First sync
|
|
155
|
+
// First sync, no manifest
|
|
156
156
|
if (!manifest || !manifest.files) {
|
|
157
157
|
// Everything remote is "new from remote"
|
|
158
158
|
for (const p of Object.keys(remoteFiles)) {
|
|
159
159
|
if (localFiles[p] && localFiles[p].hash === remoteFiles[p].hash) {
|
|
160
160
|
result.unchanged.push(p);
|
|
161
161
|
} else if (localFiles[p]) {
|
|
162
|
-
// Both exist with different hashes, no baseline
|
|
162
|
+
// Both exist with different hashes, no baseline, treat as conflict
|
|
163
163
|
result.conflicts.push(p);
|
|
164
164
|
} else {
|
|
165
165
|
result.newRemote.push(p);
|
|
@@ -191,7 +191,7 @@ function threeWayCompare(localFiles, remoteFiles, manifest) {
|
|
|
191
191
|
const baseHash = inBase ? base[p].hash : null;
|
|
192
192
|
|
|
193
193
|
if (inLocal && inRemote && inBase) {
|
|
194
|
-
// All three exist
|
|
194
|
+
// All three exist, standard three-way
|
|
195
195
|
const localChanged = localHash !== baseHash;
|
|
196
196
|
const remoteChanged = remoteHash !== baseHash;
|
|
197
197
|
|
|
@@ -223,10 +223,10 @@ function threeWayCompare(localFiles, remoteFiles, manifest) {
|
|
|
223
223
|
// Was in base, deleted locally, still remote
|
|
224
224
|
result.deletedLocal.push(p);
|
|
225
225
|
} else if (inBase && !inLocal && !inRemote) {
|
|
226
|
-
// Deleted on both sides
|
|
226
|
+
// Deleted on both sides, nothing to do
|
|
227
227
|
// (don't add to any list)
|
|
228
228
|
} else if (inLocal && inRemote && !inBase) {
|
|
229
|
-
// Both sides have it but no base
|
|
229
|
+
// Both sides have it but no base, new on both
|
|
230
230
|
if (localHash === remoteHash) {
|
|
231
231
|
result.unchanged.push(p);
|
|
232
232
|
} else {
|