@remits/remits-cli 0.1.141 → 0.1.145
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/index.js
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
- L7332 Branches, Promotion, Commit, And Test Runs
|
|
15
15
|
- L9421 Tokens, Tools, Verification, And Config
|
|
16
16
|
- L10848 Service Dashboard And WebSocket Listener
|
|
17
|
-
-
|
|
18
|
-
-
|
|
17
|
+
- L13313 Agent And Ticket Workflows
|
|
18
|
+
- L16908 Help, Auto Update, And Command Dispatch
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
/*
|
|
@@ -12615,6 +12615,327 @@ function publishMissedMessageRequest(client, baseUrl, userId) {
|
|
|
12615
12615
|
});
|
|
12616
12616
|
}
|
|
12617
12617
|
|
|
12618
|
+
function cliSessionsForUser(baseUrl, userId, accountId = null) {
|
|
12619
|
+
const normalizedBaseUrl = normalizeBaseUrl(baseUrl || DEFAULT_BASE_URL);
|
|
12620
|
+
return listSessionsFromStore(readAllSessions()).filter((session) => {
|
|
12621
|
+
const sessionUserId = session && session.user && session.user.id ? String(session.user.id) : '';
|
|
12622
|
+
if (!session || !session.token || sessionUserId !== String(userId)) {
|
|
12623
|
+
return false;
|
|
12624
|
+
}
|
|
12625
|
+
if (normalizeBaseUrl(session.baseUrl || DEFAULT_BASE_URL) !== normalizedBaseUrl) {
|
|
12626
|
+
return false;
|
|
12627
|
+
}
|
|
12628
|
+
if (accountId != null && Number(session.accountId) !== Number(accountId)) {
|
|
12629
|
+
return false;
|
|
12630
|
+
}
|
|
12631
|
+
return true;
|
|
12632
|
+
});
|
|
12633
|
+
}
|
|
12634
|
+
|
|
12635
|
+
function cliSocketSessionForUser(baseUrl, userId, accountId = null) {
|
|
12636
|
+
return cliSessionsForUser(baseUrl, userId, accountId)[0] || null;
|
|
12637
|
+
}
|
|
12638
|
+
|
|
12639
|
+
function cliTokenFingerprint(token) {
|
|
12640
|
+
if (!token) return '';
|
|
12641
|
+
return crypto.createHash('sha256').update(String(token)).digest('hex').slice(0, 16);
|
|
12642
|
+
}
|
|
12643
|
+
|
|
12644
|
+
function cliCloneSigningText(payload) {
|
|
12645
|
+
return [
|
|
12646
|
+
payload.messageId,
|
|
12647
|
+
payload.userId,
|
|
12648
|
+
payload.accountId,
|
|
12649
|
+
payload.repoName,
|
|
12650
|
+
payload.gitUrl,
|
|
12651
|
+
payload.httpsUrl,
|
|
12652
|
+
payload.branch,
|
|
12653
|
+
payload.tokenFingerprint
|
|
12654
|
+
].map((value) => value == null ? '' : String(value)).join('\n');
|
|
12655
|
+
}
|
|
12656
|
+
|
|
12657
|
+
function signCliCloneRequest(token, payload) {
|
|
12658
|
+
return crypto.createHmac('sha256', String(token)).update(cliCloneSigningText(payload)).digest('hex');
|
|
12659
|
+
}
|
|
12660
|
+
|
|
12661
|
+
function signaturesMatch(expected, actual) {
|
|
12662
|
+
const a = Buffer.from(String(expected || ''), 'hex');
|
|
12663
|
+
const b = Buffer.from(String(actual || ''), 'hex');
|
|
12664
|
+
return a.length > 0 && a.length === b.length && crypto.timingSafeEqual(a, b);
|
|
12665
|
+
}
|
|
12666
|
+
|
|
12667
|
+
function validateRepoNameForClone(repoName) {
|
|
12668
|
+
const value = String(repoName || '').trim();
|
|
12669
|
+
if (!value || value === '.' || value === '..' || !/^[A-Za-z0-9._-]+$/.test(value)) {
|
|
12670
|
+
throw new Error('Unsafe repository name in clone request');
|
|
12671
|
+
}
|
|
12672
|
+
return value;
|
|
12673
|
+
}
|
|
12674
|
+
|
|
12675
|
+
function validateGithubCloneUrl(url, repoName) {
|
|
12676
|
+
const value = String(url || '').trim();
|
|
12677
|
+
const escapedRepo = repoName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
12678
|
+
const ssh = new RegExp('^git@github\\.com:[A-Za-z0-9_.-]+/' + escapedRepo + '\\.git$');
|
|
12679
|
+
const https = new RegExp('^https://github\\.com/[A-Za-z0-9_.-]+/' + escapedRepo + '\\.git$');
|
|
12680
|
+
if (!ssh.test(value) && !https.test(value)) {
|
|
12681
|
+
throw new Error('Unexpected GitHub clone URL in clone request');
|
|
12682
|
+
}
|
|
12683
|
+
return value;
|
|
12684
|
+
}
|
|
12685
|
+
|
|
12686
|
+
function authenticatedCloneSession(baseUrl, data, payload) {
|
|
12687
|
+
const accountId = Number(payload.accountId);
|
|
12688
|
+
const requestedUserId = String(payload.userId || data.userId || '');
|
|
12689
|
+
const messageId = String(payload.messageId || payload.uuid || data.messageId || '');
|
|
12690
|
+
const expectedFingerprint = String(payload.tokenFingerprint || '').trim();
|
|
12691
|
+
if (!requestedUserId || !messageId || !expectedFingerprint || !Number.isFinite(accountId) || accountId <= 0) {
|
|
12692
|
+
throw new Error('Malformed clone request');
|
|
12693
|
+
}
|
|
12694
|
+
|
|
12695
|
+
const signingPayload = {
|
|
12696
|
+
messageId,
|
|
12697
|
+
userId: requestedUserId,
|
|
12698
|
+
accountId,
|
|
12699
|
+
repoName: payload.repoName,
|
|
12700
|
+
gitUrl: payload.gitUrl,
|
|
12701
|
+
httpsUrl: payload.httpsUrl,
|
|
12702
|
+
branch: payload.branch || 'main',
|
|
12703
|
+
tokenFingerprint: expectedFingerprint
|
|
12704
|
+
};
|
|
12705
|
+
|
|
12706
|
+
const candidateSessions = cliSessionsForUser(baseUrl, requestedUserId)
|
|
12707
|
+
.filter((session) => cliTokenFingerprint(session.token) === expectedFingerprint);
|
|
12708
|
+
|
|
12709
|
+
for (const session of candidateSessions) {
|
|
12710
|
+
const expected = signCliCloneRequest(session.token, signingPayload);
|
|
12711
|
+
if (signaturesMatch(expected, payload.cloneAuth)) {
|
|
12712
|
+
return session;
|
|
12713
|
+
}
|
|
12714
|
+
}
|
|
12715
|
+
|
|
12716
|
+
throw new Error('Unauthenticated clone request');
|
|
12717
|
+
}
|
|
12718
|
+
|
|
12719
|
+
function publishCliPresence(client, baseUrl, userIds = []) {
|
|
12720
|
+
if (!client || !client.connected) {
|
|
12721
|
+
return;
|
|
12722
|
+
}
|
|
12723
|
+
const validUserIds = Array.from(new Set((Array.isArray(userIds) ? userIds : [userIds]).filter(Boolean)));
|
|
12724
|
+
if (validUserIds.length === 0) {
|
|
12725
|
+
return;
|
|
12726
|
+
}
|
|
12727
|
+
for (const userId of validUserIds) {
|
|
12728
|
+
const session = cliSocketSessionForUser(baseUrl, userId);
|
|
12729
|
+
if (!session || !session.token) {
|
|
12730
|
+
console.warn('[listener] Skipping CLI presence for user ' + userId + ': no authenticated local session token');
|
|
12731
|
+
continue;
|
|
12732
|
+
}
|
|
12733
|
+
const repos = listAccountRepoEntries().map((entry) => ({
|
|
12734
|
+
accountId: Number(entry.accountId),
|
|
12735
|
+
name: entry.name,
|
|
12736
|
+
directory: entry.directory,
|
|
12737
|
+
branch: currentBranch(entry.directory) || 'main'
|
|
12738
|
+
}));
|
|
12739
|
+
const payload = {
|
|
12740
|
+
userId: String(userId),
|
|
12741
|
+
cliVersion: readCliVersion(),
|
|
12742
|
+
hostname: os.hostname(),
|
|
12743
|
+
platform: process.platform,
|
|
12744
|
+
scannedAt: new Date().toISOString(),
|
|
12745
|
+
repos
|
|
12746
|
+
};
|
|
12747
|
+
payload.token = session.token;
|
|
12748
|
+
try {
|
|
12749
|
+
client.publish({
|
|
12750
|
+
destination: '/app/cli-presence',
|
|
12751
|
+
body: JSON.stringify(payload)
|
|
12752
|
+
});
|
|
12753
|
+
console.log('[listener] Emitted CLI presence (' + repos.length + ' account repos for user ' + userId + ')');
|
|
12754
|
+
} catch (err) {
|
|
12755
|
+
console.error('[listener] Failed to emit CLI presence:', err.message);
|
|
12756
|
+
}
|
|
12757
|
+
}
|
|
12758
|
+
}
|
|
12759
|
+
|
|
12760
|
+
function resolveAccountRepoCloneTarget(repoName) {
|
|
12761
|
+
if (process.env.REMITS_ACCOUNT_REPOS_DIR) {
|
|
12762
|
+
return path.join(path.resolve(process.env.REMITS_ACCOUNT_REPOS_DIR), repoName);
|
|
12763
|
+
}
|
|
12764
|
+
const entries = listAccountRepoEntries();
|
|
12765
|
+
const parentCounts = {};
|
|
12766
|
+
for (const entry of entries) {
|
|
12767
|
+
if (entry.directory && fs.existsSync(entry.directory)) {
|
|
12768
|
+
const parent = path.dirname(entry.directory);
|
|
12769
|
+
parentCounts[parent] = (parentCounts[parent] || 0) + 1;
|
|
12770
|
+
}
|
|
12771
|
+
}
|
|
12772
|
+
const sortedParents = Object.entries(parentCounts).sort((a, b) => b[1] - a[1]);
|
|
12773
|
+
if (sortedParents.length > 0) {
|
|
12774
|
+
return path.join(sortedParents[0][0], repoName);
|
|
12775
|
+
}
|
|
12776
|
+
const homeProjects = path.join(os.homedir(), 'Documents', 'Projects');
|
|
12777
|
+
if (fs.existsSync(homeProjects)) {
|
|
12778
|
+
return path.join(homeProjects, 'remits_accounts', repoName);
|
|
12779
|
+
}
|
|
12780
|
+
return path.join(os.homedir(), 'remits_accounts', repoName);
|
|
12781
|
+
}
|
|
12782
|
+
|
|
12783
|
+
async function handleCliCloneRequest(client, baseUrl, topic, accountIds, userIds, data) {
|
|
12784
|
+
const payload = data.payload || data;
|
|
12785
|
+
let cloneSession = null;
|
|
12786
|
+
let accountId = Number(payload.accountId);
|
|
12787
|
+
let repoName = String(payload.repoName || '');
|
|
12788
|
+
let gitUrl = String(payload.gitUrl || '');
|
|
12789
|
+
let httpsUrl = String(payload.httpsUrl || '');
|
|
12790
|
+
const branch = String(payload.branch || 'main');
|
|
12791
|
+
const messageId = String(payload.messageId || payload.uuid || data.messageId || '');
|
|
12792
|
+
let targetUserId = String(payload.userId || data.userId || '');
|
|
12793
|
+
|
|
12794
|
+
console.log('[cli_clone_request] Received clone request for account', accountId, repoName);
|
|
12795
|
+
appendGlobalActivityLog('repo', 'clone_request.received', {
|
|
12796
|
+
baseUrl,
|
|
12797
|
+
accountId,
|
|
12798
|
+
repoName,
|
|
12799
|
+
gitUrl,
|
|
12800
|
+
messageId
|
|
12801
|
+
});
|
|
12802
|
+
|
|
12803
|
+
let targetDir = null;
|
|
12804
|
+
let cloneSuccess = false;
|
|
12805
|
+
let cloneError = null;
|
|
12806
|
+
|
|
12807
|
+
try {
|
|
12808
|
+
cloneSession = authenticatedCloneSession(baseUrl, data, payload);
|
|
12809
|
+
targetUserId = cloneSession && cloneSession.user && cloneSession.user.id ? String(cloneSession.user.id) : targetUserId;
|
|
12810
|
+
repoName = validateRepoNameForClone(payload.repoName);
|
|
12811
|
+
gitUrl = validateGithubCloneUrl(payload.gitUrl, repoName);
|
|
12812
|
+
httpsUrl = validateGithubCloneUrl(payload.httpsUrl, repoName);
|
|
12813
|
+
targetDir = resolveAccountRepoCloneTarget(repoName);
|
|
12814
|
+
|
|
12815
|
+
if (fs.existsSync(targetDir) && fs.existsSync(path.join(targetDir, '.git'))) {
|
|
12816
|
+
console.log('[cli_clone_request] Repo already exists at', targetDir);
|
|
12817
|
+
cloneSuccess = true;
|
|
12818
|
+
} else {
|
|
12819
|
+
ensureDir(path.dirname(targetDir));
|
|
12820
|
+
console.log('[cli_clone_request] Cloning into', targetDir);
|
|
12821
|
+
try {
|
|
12822
|
+
execSync('git clone ' + shellQuote(gitUrl) + ' ' + shellQuote(targetDir), { stdio: 'pipe' });
|
|
12823
|
+
cloneSuccess = true;
|
|
12824
|
+
} catch (sshErr) {
|
|
12825
|
+
console.warn('[cli_clone_request] SSH clone failed, attempting HTTPS fallback:', sshErr.message);
|
|
12826
|
+
if (httpsUrl) {
|
|
12827
|
+
try {
|
|
12828
|
+
execSync('git clone ' + shellQuote(httpsUrl) + ' ' + shellQuote(targetDir), { stdio: 'pipe' });
|
|
12829
|
+
cloneSuccess = true;
|
|
12830
|
+
} catch (httpsErr) {
|
|
12831
|
+
cloneError = httpsErr;
|
|
12832
|
+
}
|
|
12833
|
+
} else {
|
|
12834
|
+
cloneError = sshErr;
|
|
12835
|
+
}
|
|
12836
|
+
}
|
|
12837
|
+
}
|
|
12838
|
+
|
|
12839
|
+
if (!cloneSuccess) {
|
|
12840
|
+
throw cloneError || new Error('Failed to clone repository ' + repoName);
|
|
12841
|
+
}
|
|
12842
|
+
|
|
12843
|
+
// 1. Update index in ~/.remits-cli/account-repos.json
|
|
12844
|
+
updateAccountRepoIndex(targetDir);
|
|
12845
|
+
|
|
12846
|
+
// 2. Update discovery summary in ~/.remits-cli/service-state.json
|
|
12847
|
+
recordDiscoverySummary(discoverAccountRepos());
|
|
12848
|
+
|
|
12849
|
+
// 3. Download front-stage guides & agent docs into targetDir
|
|
12850
|
+
const sessions = listSessionsFromStore(readAllSessions());
|
|
12851
|
+
const session = guideSessionForRepo({ accountId }, sessions) || { baseUrl };
|
|
12852
|
+
try {
|
|
12853
|
+
await downloadAndSyncGuides(targetDir, { session, baseUrl, accountId });
|
|
12854
|
+
} catch (gErr) {
|
|
12855
|
+
console.warn('[cli_clone_request] Guide sync failed:', gErr.message);
|
|
12856
|
+
}
|
|
12857
|
+
|
|
12858
|
+
// 4. Install / refresh agent skills for Claude, Codex, Gemini
|
|
12859
|
+
try {
|
|
12860
|
+
syncInstalledSkills('all');
|
|
12861
|
+
} catch (_) {}
|
|
12862
|
+
|
|
12863
|
+
// 5. Append to global activity log
|
|
12864
|
+
appendGlobalActivityLog('repo', 'clone.success', {
|
|
12865
|
+
accountId,
|
|
12866
|
+
repoName,
|
|
12867
|
+
directory: targetDir
|
|
12868
|
+
});
|
|
12869
|
+
|
|
12870
|
+
console.log('[cli_clone_request] Successfully cloned and initialized repo at', targetDir);
|
|
12871
|
+
|
|
12872
|
+
// 6. Publish updated presence so server and browser know immediately
|
|
12873
|
+
publishCliPresence(client, baseUrl, userIds);
|
|
12874
|
+
|
|
12875
|
+
// 7. Publish completion notification to user
|
|
12876
|
+
const completionPayload = {
|
|
12877
|
+
messageId,
|
|
12878
|
+
uuid: messageId,
|
|
12879
|
+
type: 'cli_clone_completed',
|
|
12880
|
+
accountId,
|
|
12881
|
+
repoName,
|
|
12882
|
+
directory: targetDir,
|
|
12883
|
+
branch,
|
|
12884
|
+
success: true
|
|
12885
|
+
};
|
|
12886
|
+
|
|
12887
|
+
try {
|
|
12888
|
+
client.publish({
|
|
12889
|
+
destination: '/app/cli-event',
|
|
12890
|
+
body: JSON.stringify({
|
|
12891
|
+
...completionPayload,
|
|
12892
|
+
userId: targetUserId,
|
|
12893
|
+
token: cloneSession.token
|
|
12894
|
+
})
|
|
12895
|
+
});
|
|
12896
|
+
client.publish({
|
|
12897
|
+
destination: '/app/reply',
|
|
12898
|
+
body: JSON.stringify(completionPayload)
|
|
12899
|
+
});
|
|
12900
|
+
} catch (_) {}
|
|
12901
|
+
|
|
12902
|
+
} catch (err) {
|
|
12903
|
+
console.error('[cli_clone_request] Error cloning repo:', err.message);
|
|
12904
|
+
appendGlobalActivityLog('repo', 'clone.error', {
|
|
12905
|
+
accountId,
|
|
12906
|
+
repoName,
|
|
12907
|
+
error: err.message
|
|
12908
|
+
}, 'error');
|
|
12909
|
+
|
|
12910
|
+
const errorPayload = {
|
|
12911
|
+
messageId,
|
|
12912
|
+
uuid: messageId,
|
|
12913
|
+
type: 'cli_clone_failed',
|
|
12914
|
+
accountId,
|
|
12915
|
+
repoName,
|
|
12916
|
+
success: false,
|
|
12917
|
+
error: err.message
|
|
12918
|
+
};
|
|
12919
|
+
|
|
12920
|
+
try {
|
|
12921
|
+
if (cloneSession && cloneSession.token) {
|
|
12922
|
+
client.publish({
|
|
12923
|
+
destination: '/app/cli-event',
|
|
12924
|
+
body: JSON.stringify({
|
|
12925
|
+
...errorPayload,
|
|
12926
|
+
userId: targetUserId,
|
|
12927
|
+
token: cloneSession.token
|
|
12928
|
+
})
|
|
12929
|
+
});
|
|
12930
|
+
}
|
|
12931
|
+
client.publish({
|
|
12932
|
+
destination: '/app/reply',
|
|
12933
|
+
body: JSON.stringify(errorPayload)
|
|
12934
|
+
});
|
|
12935
|
+
} catch (_) {}
|
|
12936
|
+
}
|
|
12937
|
+
}
|
|
12938
|
+
|
|
12618
12939
|
function acknowledgeWebsocketMessage(client, messageId, userIds = []) {
|
|
12619
12940
|
if (!messageId) {
|
|
12620
12941
|
return;
|
|
@@ -12697,6 +13018,8 @@ function processInboundWebsocketMessage(client, baseUrl, topic, accountIds, user
|
|
|
12697
13018
|
});
|
|
12698
13019
|
supportTicketSnapshotCache = { fetchedAtMs: 0, value: null };
|
|
12699
13020
|
agentSnapshotCache = { fetchedAtMs: 0, value: null };
|
|
13021
|
+
} else if (data.type === 'cli_clone_request') {
|
|
13022
|
+
handleCliCloneRequest(client, baseUrl, topic, accountIds, userIds, data);
|
|
12700
13023
|
}
|
|
12701
13024
|
|
|
12702
13025
|
updateRuntimeServiceState();
|
|
@@ -12710,7 +13033,8 @@ function createManagedWebsocketClient(baseUrl, urlSessions) {
|
|
|
12710
13033
|
client: null,
|
|
12711
13034
|
lastConnectAttemptAt: null,
|
|
12712
13035
|
lastConnectedAt: null,
|
|
12713
|
-
lastDisconnectedAt: null
|
|
13036
|
+
lastDisconnectedAt: null,
|
|
13037
|
+
lastPresenceAt: null
|
|
12714
13038
|
};
|
|
12715
13039
|
|
|
12716
13040
|
ensureRuntimeWebsocketEntry(normalizedBaseUrl);
|
|
@@ -12814,6 +13138,9 @@ function createManagedWebsocketClient(baseUrl, urlSessions) {
|
|
|
12814
13138
|
for (const userId of userIds) {
|
|
12815
13139
|
publishMissedMessageRequest(client, normalizedBaseUrl, userId);
|
|
12816
13140
|
}
|
|
13141
|
+
|
|
13142
|
+
publishCliPresence(client, normalizedBaseUrl, userIds);
|
|
13143
|
+
managedClient.lastPresenceAt = Date.now();
|
|
12817
13144
|
};
|
|
12818
13145
|
|
|
12819
13146
|
client.onDisconnect = () => {
|
|
@@ -12926,6 +13253,21 @@ async function runManagedWebsocketWatchdog() {
|
|
|
12926
13253
|
const state = ensureRuntimeWebsocketEntry(managedClient.baseUrl);
|
|
12927
13254
|
const socket = managedClient.client.webSocket;
|
|
12928
13255
|
const readyState = socket && typeof socket.readyState === 'number' ? socket.readyState : null;
|
|
13256
|
+
|
|
13257
|
+
if (state.status === 'connected' && managedClient.client.connected) {
|
|
13258
|
+
if (!managedClient.lastPresenceAt || (now - managedClient.lastPresenceAt) >= 30000) {
|
|
13259
|
+
managedClient.lastPresenceAt = now;
|
|
13260
|
+
const userIds = [];
|
|
13261
|
+
for (const session of (managedClient.urlSessions || [])) {
|
|
13262
|
+
const userId = session && session.user && session.user.id ? String(session.user.id) : null;
|
|
13263
|
+
if (userId && !userIds.includes(userId)) {
|
|
13264
|
+
userIds.push(userId);
|
|
13265
|
+
}
|
|
13266
|
+
}
|
|
13267
|
+
publishCliPresence(managedClient.client, managedClient.baseUrl, userIds);
|
|
13268
|
+
}
|
|
13269
|
+
}
|
|
13270
|
+
|
|
12929
13271
|
const disconnectedAtMs = state.disconnectedAt ? Date.parse(state.disconnectedAt) : 0;
|
|
12930
13272
|
const lastConnectAttemptAge = managedClient.lastConnectAttemptAt ? (now - managedClient.lastConnectAttemptAt) : 0;
|
|
12931
13273
|
const connectedReadyMismatch = state.status === 'connected' && readyState != null && readyState !== WebSocket.OPEN;
|
package/package.json
CHANGED
|
@@ -98,6 +98,12 @@ reference named after it.
|
|
|
98
98
|
not run JavaScript, Alpine, CSS/assets, redirects, cookies/session handling, uploads, iframes,
|
|
99
99
|
shadow-DOM placement, or websocket behavior. (`development-loop.md`,
|
|
100
100
|
`docs/front-stage/components/embeddable-components.md`, `docs/front-stage/features/web-browser-automation.md`)
|
|
101
|
+
- **Browser automation cleanup is part of the task.** When you finish any browser automation you
|
|
102
|
+
started, close Playwright pages/contexts/browsers, stop any `playwright-cli` daemon/session you
|
|
103
|
+
started, remove temporary Playwright session/profile state when safe, and verify no task-owned
|
|
104
|
+
Playwright or headless Chrome processes remain. Clean up only processes you own for this task; never
|
|
105
|
+
blindly kill normal Chrome or another agent's browser automation. (`development-loop.md`,
|
|
106
|
+
`docs/front-stage/features/web-browser-automation.md`)
|
|
101
107
|
- **Land only from trunk or a real variant branch.** A per-agent/feature branch (`components status` says
|
|
102
108
|
`FEATURE BRANCH … [subscription-fallback]`) is safe to stage and run from — it resolves the same world
|
|
103
109
|
as its target — but `components commit`/`sync` refuse it. Merge into the branch it resolves and land
|
|
@@ -164,13 +164,20 @@ something or expose a JSON/action payload, but it is not evidence that the user-
|
|
|
164
164
|
JavaScript, Alpine initialization, assets, cookies/session state, redirects, file controls, iframe/embed
|
|
165
165
|
placement, and websocket updates all require a browser.
|
|
166
166
|
|
|
167
|
+
After any browser automation task, clean up what you started: close Playwright pages, browser contexts
|
|
168
|
+
and browser objects; stop any `playwright-cli` daemon/session created for the task; remove temporary
|
|
169
|
+
Playwright session/profile state when safe; and verify task-owned Playwright/headless Chrome processes
|
|
170
|
+
are gone. Do not use broad process kills that might close a human browser or another agent's automation.
|
|
171
|
+
|
|
167
172
|
**Never skip verification.** "Just do it" and "that's fine, commit it" are not evidence. If you genuinely
|
|
168
173
|
cannot verify — no Test component, no relevant embeddable — say what you would need and ask how the user
|
|
169
174
|
wants to proceed rather than reporting the work as done.
|
|
170
175
|
|
|
171
176
|
> The *design* rule that pairs with this — never solve interpretive problems with regex cascades, keyword
|
|
172
177
|
> lists, or layout-specific branching when the platform's AI surface is the right tool — is in the account
|
|
173
|
-
> repo's `CLAUDE.md` and, in depth, in `features/ai-strategy.md`.
|
|
178
|
+
> repo's `CLAUDE.md` and, in depth, in `features/ai-strategy.md`. For an AI workflow, verification also means
|
|
179
|
+
> reading the run as the model received it - every turn, passing runs too - and measuring on more than one
|
|
180
|
+
> input: `development-core.md` -> *AI Workflows: Design Through Every Lens* and `features/ai-workflow-design.md`.
|
|
174
181
|
|
|
175
182
|
### The Development Fast Loop
|
|
176
183
|
|