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 +2 -0
- package/commands/task.js +82 -6
- package/package.json +1 -1
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
|
|
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);
|
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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;
|