phewsh 0.15.70 → 0.15.71
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/commands/task.js +61 -3
- package/package.json +1 -1
package/commands/task.js
CHANGED
|
@@ -123,11 +123,41 @@ function runnerArgs(harnessId, prompt) {
|
|
|
123
123
|
return HARNESSES[harnessId].args(prompt);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
async function memberNames(config, projectId) {
|
|
127
|
+
try {
|
|
128
|
+
const rows = await supa.rpc('project_member_names', { pid: projectId }, config.supabaseAccessToken);
|
|
129
|
+
const m = {};
|
|
130
|
+
for (const r of rows || []) m[r.user_id] = r.display_name;
|
|
131
|
+
return m;
|
|
132
|
+
} catch { return {}; }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Close the loop from verified GitHub state: any listed task whose PR has
|
|
136
|
+
// actually merged gets recorded as merged (best-effort, never blocks the list).
|
|
137
|
+
function syncMergedFromGitHub(config, rows) {
|
|
138
|
+
const candidates = rows.filter((t) => ['pr_open', 'approved'].includes(t.status) && t.pull_request_url);
|
|
139
|
+
const updated = [];
|
|
140
|
+
for (const t of candidates) {
|
|
141
|
+
try {
|
|
142
|
+
const out = execFileSync('gh', ['pr', 'view', t.pull_request_url, '--json', 'state'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
143
|
+
if (JSON.parse(out).state === 'MERGED') updated.push(t);
|
|
144
|
+
} catch { /* gh missing or PR unreachable — leave as-is */ }
|
|
145
|
+
}
|
|
146
|
+
return Promise.all(updated.map((t) =>
|
|
147
|
+
supa.rpc('mark_merged', { p_task_id: t.id, p_metadata: { via: 'gh pr view' } }, config.supabaseAccessToken)
|
|
148
|
+
.then(() => { t.status = 'merged'; })
|
|
149
|
+
.catch(() => {})
|
|
150
|
+
));
|
|
151
|
+
}
|
|
152
|
+
|
|
126
153
|
async function listTasks(config) {
|
|
127
154
|
const project = await loadProject(config);
|
|
128
155
|
const rows = await supa.select('tasks',
|
|
129
|
-
`project_id=eq.${project.id}&select=id,title,status,claimed_by,pull_request_url,created_at&order=created_at.desc&limit=20`,
|
|
156
|
+
`project_id=eq.${project.id}&select=id,title,status,claimed_by,assigned_to,suggested_harness,pull_request_url,created_at&order=created_at.desc&limit=20`,
|
|
130
157
|
config.supabaseAccessToken);
|
|
158
|
+
await syncMergedFromGitHub(config, rows);
|
|
159
|
+
const names = await memberNames(config, project.id);
|
|
160
|
+
const nameOf = (id) => id === config.supabaseUserId ? 'you' : (names[id] || 'a teammate');
|
|
131
161
|
console.log(`\n ${b(w(project.name))} ${g('— shared tasks')}\n`);
|
|
132
162
|
if (!rows.length) {
|
|
133
163
|
console.log(` ${g('No tasks yet.')} ${w('phewsh task new "<title>"')} ${g('to request one.')}\n`);
|
|
@@ -135,13 +165,40 @@ async function listTasks(config) {
|
|
|
135
165
|
}
|
|
136
166
|
for (const t of rows) {
|
|
137
167
|
const color = STATUS_COLOR[t.status] || w;
|
|
138
|
-
const
|
|
139
|
-
|
|
168
|
+
const bits = [];
|
|
169
|
+
if (t.claimed_by) bits.push(`claimed by ${nameOf(t.claimed_by)}`);
|
|
170
|
+
else if (t.assigned_to) bits.push(`assigned to ${nameOf(t.assigned_to)}`);
|
|
171
|
+
if (t.suggested_harness && !t.claimed_by) bits.push(`suggests ${HARNESSES[t.suggested_harness]?.label || t.suggested_harness}`);
|
|
172
|
+
console.log(` ${color('●')} ${w(t.title)} ${g(t.id.slice(0, 8))} ${color(t.status)}${bits.length ? g(' · ' + bits.join(' · ')) : ''}`);
|
|
140
173
|
if (t.pull_request_url) console.log(` ${cyan(t.pull_request_url)}`);
|
|
174
|
+
if (t.status === 'merged') console.log(` ${g('merged — record it:')} ${w(`phewsh task reconcile ${t.id.slice(0, 8)}`)}`);
|
|
141
175
|
}
|
|
142
176
|
console.log(`\n ${g('Claim one:')} ${w('phewsh task claim <id>')}\n`);
|
|
143
177
|
}
|
|
144
178
|
|
|
179
|
+
// Promote a merged task into the repo's durable Record (.intent/decisions.md)
|
|
180
|
+
// and mark it reconciled — the last beat of the loop.
|
|
181
|
+
async function reconcileTask(config, idArg) {
|
|
182
|
+
if (!idArg) throw new Error('Usage: phewsh task reconcile <task-id>');
|
|
183
|
+
const project = await loadProject(config);
|
|
184
|
+
const rows = await supa.select('tasks', `project_id=eq.${project.id}&id=like.${idArg}*&select=*`, config.supabaseAccessToken);
|
|
185
|
+
const task = rows[0];
|
|
186
|
+
if (!task) throw new Error(`Task ${idArg} not found in this project.`);
|
|
187
|
+
if (task.status !== 'merged') {
|
|
188
|
+
await syncMergedFromGitHub(config, [task]);
|
|
189
|
+
if (task.status !== 'merged') throw new Error(`Task is ${task.status} — only merged tasks can be reconciled.`);
|
|
190
|
+
}
|
|
191
|
+
const names = await memberNames(config, project.id);
|
|
192
|
+
const nameOf = (id) => names[id] || 'a teammate';
|
|
193
|
+
const decisionsPath = path.join(process.cwd(), '.intent', 'decisions.md');
|
|
194
|
+
const line = `- ${new Date().toISOString().slice(0, 10)} — Shared task "${task.title}" (${task.id.slice(0, 8)}) merged and recorded: requested by ${nameOf(task.created_by)}, executed by ${nameOf(task.claimed_by)}, reviewed on GitHub. PR: ${task.pull_request_url || 'n/a'}\n`;
|
|
195
|
+
if (fs.existsSync(decisionsPath)) fs.appendFileSync(decisionsPath, line);
|
|
196
|
+
else console.log(` ${g('(no .intent/decisions.md here — recording in the cloud only)')}`);
|
|
197
|
+
await supa.rpc('mark_reconciled', { p_task_id: task.id }, config.supabaseAccessToken);
|
|
198
|
+
console.log(`\n ${green('✓')} Recorded into project truth: ${w(task.title)}`);
|
|
199
|
+
if (fs.existsSync(decisionsPath)) console.log(` ${g('Appended to .intent/decisions.md — commit it with your normal flow.')}\n`);
|
|
200
|
+
}
|
|
201
|
+
|
|
145
202
|
async function inviteTeammate(config, email) {
|
|
146
203
|
if (!email || !email.includes('@')) throw new Error('Usage: phewsh task invite <email>');
|
|
147
204
|
const project = await loadProject(config);
|
|
@@ -296,6 +353,7 @@ module.exports = async function run() {
|
|
|
296
353
|
if (sub === 'claim') return await claimTask(config, rest[0], via);
|
|
297
354
|
if (sub === 'invite') return await inviteTeammate(config, rest[0]);
|
|
298
355
|
if (sub === 'join') return await joinProjects(config);
|
|
356
|
+
if (sub === 'reconcile') return await reconcileTask(config, rest[0]);
|
|
299
357
|
console.log(`\n Usage: phewsh task [list | new "<title>" | claim <id> [--via <harness>] | invite <email> | join]\n phewsh dispatch ["<title>" | <id> | next] [--via <harness>]\n`);
|
|
300
358
|
} catch (err) {
|
|
301
359
|
console.error(`\n ${red('✗')} ${err.message}\n`);
|