phewsh 0.15.69 → 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/bin/phewsh.js CHANGED
@@ -112,6 +112,7 @@ const COMMANDS = {
112
112
  work: () => require('../commands/work')(),
113
113
  remember: () => require('../commands/remember')(),
114
114
  task: () => require('../commands/task')(),
115
+ dispatch: () => require('../commands/task')(),
115
116
  pack: () => require('../commands/pack')(),
116
117
  hook: () => require('../commands/hook')(),
117
118
  welcome: () => require('../commands/welcome')(),
@@ -163,6 +164,7 @@ function showHelp() {
163
164
  console.log(` ${cyan('shim')} ${g('Guaranteed launch banner — phewsh prints status before each tool')}`);
164
165
  console.log(` ${cyan('pack')} ${g('Opt-in workflow packs (Karpathy guidelines, GSD…) — attributed, reversible')}`);
165
166
  console.log(` ${cyan('task')} ${g('Shared tasks — request, claim, and ship teammate work via branch + PR')}
167
+ ${cyan('dispatch')} ${g('Friendly verb over task: dispatch "<title>" to request, <id>|next to claim')}
166
168
  ${cyan('receipts')} ${g('Proof trail — what agents actually did, with evidence')}`);
167
169
  console.log(` ${cyan('remember')} ${g('Jot a decision to .intent/decisions.md — every tool inherits it')}`);
168
170
  console.log(` ${cyan('outcomes')} ${g('Decision record — what was kept, reverted, or failed')}`);
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);
@@ -193,7 +250,13 @@ async function claimTask(config, idArg, viaFlag) {
193
250
  throw new Error('`gh` is not installed or not authenticated — needed to open the PR. Run `gh auth login`.');
194
251
  }
195
252
 
196
- // Resolve a short id prefix to the full task id.
253
+ // `claim next` = the oldest open task; otherwise resolve a short id prefix.
254
+ if (idArg === 'next') {
255
+ const open = await supa.select('tasks',
256
+ `project_id=eq.${project.id}&status=eq.open&select=id&order=created_at.asc&limit=1`, config.supabaseAccessToken);
257
+ if (!open.length) throw new Error('No open tasks to claim.');
258
+ idArg = open[0].id;
259
+ }
197
260
  const candidates = await supa.select('tasks',
198
261
  `project_id=eq.${project.id}&id=like.${idArg}*&select=*`, config.supabaseAccessToken)
199
262
  .catch(() => []);
@@ -266,8 +329,18 @@ async function claimTask(config, idArg, viaFlag) {
266
329
  console.log(` ${g('Clean up later:')} ${w(`git worktree remove ${worktree}`)}\n`);
267
330
  }
268
331
 
332
+ // `phewsh dispatch` is the friendly verb over the same machinery — no second
333
+ // architecture. Bare = list; an id prefix = claim; anything else = new task.
334
+ function dispatchToTaskArgs(args) {
335
+ const positional = args.filter((a, i) => !a.startsWith('--') && !(i > 0 && args[i - 1].startsWith('--')));
336
+ if (!positional.length) return ['list'];
337
+ if (positional.length === 1 && (positional[0] === 'next' || /^[0-9a-f-]{6,36}$/i.test(positional[0]))) return ['claim', ...args];
338
+ return ['new', ...args];
339
+ }
340
+
269
341
  module.exports = async function run() {
270
- const args = process.argv.slice(3);
342
+ let args = process.argv.slice(3);
343
+ if (process.argv[2] === 'dispatch') args = dispatchToTaskArgs(args);
271
344
  const sub = args[0] || 'list';
272
345
  const viaIdx = args.indexOf('--via');
273
346
  const via = viaIdx !== -1 ? args[viaIdx + 1] : null;
@@ -280,9 +353,12 @@ module.exports = async function run() {
280
353
  if (sub === 'claim') return await claimTask(config, rest[0], via);
281
354
  if (sub === 'invite') return await inviteTeammate(config, rest[0]);
282
355
  if (sub === 'join') return await joinProjects(config);
283
- console.log(`\n Usage: phewsh task [list | new "<title>" | claim <id> [--via <harness>] | invite <email> | join]\n`);
356
+ if (sub === 'reconcile') return await reconcileTask(config, rest[0]);
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`);
284
358
  } catch (err) {
285
359
  console.error(`\n ${red('✗')} ${err.message}\n`);
286
360
  process.exitCode = 1;
287
361
  }
288
362
  };
363
+
364
+ module.exports.dispatchToTaskArgs = dispatchToTaskArgs;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.15.69",
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"