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.
Files changed (2) hide show
  1. package/commands/task.js +61 -3
  2. 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 mine = t.claimed_by === config.supabaseUserId ? g(' · claimed by you') : '';
139
- console.log(` ${color('●')} ${w(t.title)} ${g(t.id.slice(0, 8))} ${color(t.status)}${mine}`);
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`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.15.70",
3
+ "version": "0.15.71",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"