@papillon6814/pi-gh 0.0.0-stage → 0.2.0

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 (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +134 -2
  3. package/dist/src/core/approval.d.ts +2 -0
  4. package/dist/src/core/approval.js +25 -0
  5. package/dist/src/core/data.d.ts +19 -0
  6. package/dist/src/core/data.js +136 -0
  7. package/dist/src/core/draft.d.ts +6 -0
  8. package/dist/src/core/draft.js +92 -0
  9. package/dist/src/core/forms.d.ts +3 -0
  10. package/dist/src/core/forms.js +54 -0
  11. package/dist/src/core/gh-process.d.ts +14 -0
  12. package/dist/src/core/gh-process.js +62 -0
  13. package/dist/src/core/issue-labels.d.ts +9 -0
  14. package/dist/src/core/issue-labels.js +52 -0
  15. package/dist/src/core/labels-apply.d.ts +12 -0
  16. package/dist/src/core/labels-apply.js +73 -0
  17. package/dist/src/core/labels-approval.d.ts +10 -0
  18. package/dist/src/core/labels-approval.js +12 -0
  19. package/dist/src/core/labels-github.d.ts +54 -0
  20. package/dist/src/core/labels-github.js +114 -0
  21. package/dist/src/core/labels-input.d.ts +31 -0
  22. package/dist/src/core/labels-input.js +58 -0
  23. package/dist/src/core/render.d.ts +6 -0
  24. package/dist/src/core/render.js +21 -0
  25. package/dist/src/core/secrets.d.ts +9 -0
  26. package/dist/src/core/secrets.js +35 -0
  27. package/dist/src/core/submit.d.ts +3 -0
  28. package/dist/src/core/submit.js +37 -0
  29. package/dist/src/core/template.d.ts +2 -0
  30. package/dist/src/core/template.js +80 -0
  31. package/dist/src/core/types.d.ts +95 -0
  32. package/dist/src/core/types.js +1 -0
  33. package/dist/src/github-api.d.ts +28 -0
  34. package/dist/src/github-api.js +100 -0
  35. package/dist/src/operation-input.d.ts +35 -0
  36. package/dist/src/operation-input.js +88 -0
  37. package/dist/src/operations.d.ts +3 -0
  38. package/dist/src/operations.js +137 -0
  39. package/dist/src/paths.d.ts +4 -0
  40. package/dist/src/paths.js +21 -0
  41. package/dist/src/permissions.d.ts +10 -0
  42. package/dist/src/permissions.js +100 -0
  43. package/dist/src/result.d.ts +4 -0
  44. package/dist/src/result.js +5 -0
  45. package/dist/src/service-types.d.ts +37 -0
  46. package/dist/src/service-types.js +1 -0
  47. package/dist/src/service.d.ts +4 -0
  48. package/dist/src/service.js +123 -0
  49. package/examples/labels-create.json +8 -0
  50. package/examples/labels-delete.json +6 -0
  51. package/examples/labels-edit.json +9 -0
  52. package/examples/labels-issue.json +12 -0
  53. package/examples/parent.json +37 -0
  54. package/examples/task.json +51 -0
  55. package/extensions/approval-ui.ts +41 -0
  56. package/extensions/index.ts +12 -0
  57. package/extensions/runtime-scope.ts +15 -0
  58. package/extensions/tools.ts +53 -0
  59. package/package.json +51 -4
  60. package/templates/parent.yml +47 -0
  61. package/templates/policies/models.yml +20 -0
  62. package/templates/task.yml +71 -0
@@ -0,0 +1,5 @@
1
+ import { DataError } from './core/data.js';
2
+ import { maskDecodedSecrets } from './core/secrets.js';
3
+ export function safeResult(result) { return maskDecodedSecrets(result).value; }
4
+ export function rejected(code, message) { return safeResult({ status: 'rejected', problems: [{ code, path: '', message }] }); }
5
+ export function errorResult(error) { return error instanceof DataError ? safeResult({ status: 'rejected', problems: [{ code: error.code, path: error.field, message: error.message }] }) : rejected('OPERATION_FAILED', 'Operation could not be confirmed.'); }
@@ -0,0 +1,37 @@
1
+ import type { ValidationProblem } from './core/types.js';
2
+ import type { PermissionLease } from './permissions.js';
3
+ export type Operation = 'gh_issue_validate' | 'gh_issue_preview' | 'gh_issue_submit' | 'gh_labels_validate' | 'gh_labels_preview' | 'gh_labels_apply' | 'gh_issue_form';
4
+ export type IssueArgs = {
5
+ draftPath: string;
6
+ template?: 'parent' | 'task';
7
+ templatePath?: string;
8
+ };
9
+ export type LabelsArgs = {
10
+ changePath: string;
11
+ };
12
+ export type FormArgs = {
13
+ template?: 'parent' | 'task';
14
+ templatePath?: string;
15
+ };
16
+ export type ApprovalView = {
17
+ operation: string;
18
+ text: string;
19
+ digest: string;
20
+ };
21
+ export type OperationContext = {
22
+ cwd: string;
23
+ packageRoot: string;
24
+ interactive: boolean;
25
+ permissions?: PermissionLease;
26
+ signal: AbortSignal;
27
+ stamp: string;
28
+ isCurrent: () => boolean;
29
+ confirm: (view: ApprovalView) => Promise<boolean>;
30
+ };
31
+ export type OperationResult = {
32
+ status: string;
33
+ data?: unknown;
34
+ problems?: ValidationProblem[];
35
+ message?: string;
36
+ candidateUrl?: string;
37
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ import type { Operation, IssueArgs, LabelsArgs, FormArgs, OperationContext, OperationResult, ApprovalView } from './service-types.js';
2
+ export declare function authorize(view: ApprovalView, ctx: OperationContext, repo: string, permissionOperation?: string, projectId?: string): Promise<boolean>;
3
+ export declare function recheckAuthority(ctx: OperationContext, machine: boolean): Promise<void>;
4
+ export declare function runOperation(operation: Operation, args: IssueArgs | LabelsArgs | FormArgs, ctx: OperationContext): Promise<OperationResult>;
@@ -0,0 +1,123 @@
1
+ import { loadDraft, validateDraft } from './core/draft.js';
2
+ import { loadTemplate } from './core/template.js';
3
+ import { renderForm } from './core/forms.js';
4
+ import { createIssuePreview } from './core/issue-labels.js';
5
+ import { submitIssue } from './core/submit.js';
6
+ import { validateLabelChange } from './core/labels-input.js';
7
+ import { previewLabelChange } from './core/labels-approval.js';
8
+ import { applyLabelChange } from './core/labels-apply.js';
9
+ import { maskDecodedSecrets } from './core/secrets.js';
10
+ import { fail, parseJson } from './core/data.js';
11
+ import { validateArguments, templatePath, inputPath } from './paths.js';
12
+ import { safeResult, rejected, errorResult } from './result.js';
13
+ const operations = ['gh_issue_validate', 'gh_issue_preview', 'gh_issue_submit', 'gh_labels_validate', 'gh_labels_preview', 'gh_labels_apply', 'gh_issue_form'];
14
+ function active(ctx) { return !ctx.signal.aborted && ctx.isCurrent(); }
15
+ function requireActive(ctx) { if (!active(ctx))
16
+ fail('STALE_OPERATION', '', 'Operation was cancelled or its session changed.'); }
17
+ async function confirm(view, ctx) {
18
+ if (!active(ctx))
19
+ return false;
20
+ let listener = () => { };
21
+ const cancelled = new Promise(resolve => { listener = () => resolve(false); ctx.signal.addEventListener('abort', listener, { once: true }); });
22
+ try {
23
+ return await Promise.race([Promise.resolve().then(() => active(ctx) ? ctx.confirm(view) : false), cancelled]);
24
+ }
25
+ finally {
26
+ ctx.signal.removeEventListener('abort', listener);
27
+ }
28
+ }
29
+ export async function authorize(view, ctx, repo, permissionOperation = view.operation, projectId) {
30
+ requireActive(ctx);
31
+ if (ctx.permissions?.allows(permissionOperation, repo, projectId)) {
32
+ if (!await ctx.permissions.isCurrent())
33
+ fail('PERMISSION_CHANGED', '', 'Permission file changed.');
34
+ return true;
35
+ }
36
+ if (!ctx.interactive || !await confirm(view, ctx))
37
+ fail('APPROVAL_DENIED', '', 'Change was not approved.');
38
+ return false;
39
+ }
40
+ export async function recheckAuthority(ctx, machine) {
41
+ requireActive(ctx);
42
+ if (machine && (!ctx.permissions || !await ctx.permissions.isCurrent()))
43
+ fail('PERMISSION_CHANGED', '', 'Permission file changed.');
44
+ requireActive(ctx);
45
+ }
46
+ async function issue(args, ctx, withPreview = true) {
47
+ const input = await loadDraft(inputPath(args.draftPath, ctx)), bundle = await loadTemplate(templatePath(args, ctx)), checked = validateDraft(input, bundle);
48
+ if (!checked.issue)
49
+ return { checked };
50
+ if (!withPreview)
51
+ return { checked };
52
+ const sensitive = maskDecodedSecrets(parseJson(input.bytes)).sensitive || maskDecodedSecrets({ template: bundle.template, policy: bundle.policy }).sensitive;
53
+ const preview = await createIssuePreview(checked.issue, { signal: ctx.signal, beforeStart: () => active(ctx) });
54
+ return { checked, preview: { ...preview, sensitive: preview.sensitive || sensitive } };
55
+ }
56
+ export async function runOperation(operation, args, ctx) {
57
+ try {
58
+ if (!operations.includes(operation))
59
+ return rejected('ARGUMENT', 'Unknown operation.');
60
+ validateArguments(operation, args);
61
+ requireActive(ctx);
62
+ const write = operation === 'gh_issue_submit' || operation === 'gh_labels_apply';
63
+ if (write && !ctx.interactive && !ctx.permissions)
64
+ return rejected('APPROVAL_UI_REQUIRED', 'External changes require a local human confirmation screen.');
65
+ let machineAuthority = false;
66
+ const options = { signal: ctx.signal, beforeStart: () => active(ctx), beforeStartAsync: async () => !machineAuthority || !!ctx.permissions && await ctx.permissions.isCurrent() };
67
+ if (operation === 'gh_issue_form') {
68
+ const bundle = await loadTemplate(templatePath(args, ctx));
69
+ requireActive(ctx);
70
+ const masked = maskDecodedSecrets({ template: bundle.template, policy: bundle.policy });
71
+ return safeResult({ status: 'generated', data: { yaml: renderForm({ ...bundle, ...masked.value }) } });
72
+ }
73
+ if (operation.startsWith('gh_labels_')) {
74
+ const path = inputPath(args.changePath, ctx), input = await loadDraft(path);
75
+ validateLabelChange(input);
76
+ if (operation === 'gh_labels_validate')
77
+ return safeResult({ status: 'validated' });
78
+ const preview = await previewLabelChange(input, options);
79
+ requireActive(ctx);
80
+ const sensitive = preview.sensitive || maskDecodedSecrets(parseJson(input.bytes)).sensitive;
81
+ const displayed = { ...preview, sensitive };
82
+ if (operation === 'gh_labels_preview')
83
+ return safeResult({ status: 'preview', data: displayed });
84
+ if (sensitive)
85
+ return rejected('SENSITIVE', 'Remove secret candidates before applying.');
86
+ const change = validateLabelChange(input), permissionOperation = change.operation === 'label-delete' ? 'forbidden-delete' : change.operation === 'issue-labels' ? 'gh_issue_labels' : change.operation === 'label-create' ? 'gh_label_create' : 'gh_label_edit';
87
+ const machine = await authorize({ operation, text: JSON.stringify(maskDecodedSecrets(displayed).value, null, 2), digest: preview.digest }, ctx, change.repo, permissionOperation);
88
+ machineAuthority = machine;
89
+ requireActive(ctx);
90
+ const latest = await loadDraft(path), next = await previewLabelChange(latest, options);
91
+ requireActive(ctx);
92
+ if (next.digest !== preview.digest || next.sensitive || maskDecodedSecrets(parseJson(latest.bytes)).sensitive)
93
+ return rejected('APPROVAL_MISMATCH', 'Input or GitHub state changed; preview and approve again.');
94
+ await recheckAuthority(ctx, machine);
95
+ const result = await applyLabelChange(latest, preview.digest, options);
96
+ return safeResult({ ...result });
97
+ }
98
+ const captured = await issue(args, ctx, operation !== 'gh_issue_validate');
99
+ requireActive(ctx);
100
+ if (!captured.checked.issue)
101
+ return safeResult({ status: 'rejected', problems: captured.checked.problems });
102
+ if (operation === 'gh_issue_validate')
103
+ return safeResult({ status: 'validated' });
104
+ const preview = captured.preview;
105
+ if (operation === 'gh_issue_preview')
106
+ return safeResult({ status: 'preview', data: preview });
107
+ if (preview.sensitive)
108
+ return rejected('SENSITIVE', 'Remove secret candidates before submitting.');
109
+ const machine = await authorize({ operation, text: JSON.stringify(maskDecodedSecrets(preview).value, null, 2), digest: preview.digest }, ctx, preview.repo);
110
+ machineAuthority = machine;
111
+ requireActive(ctx);
112
+ const latest = await issue(args, ctx);
113
+ requireActive(ctx);
114
+ if (!latest.checked.issue || latest.preview.digest !== preview.digest || latest.preview.sensitive)
115
+ return rejected('APPROVAL_MISMATCH', 'Input or GitHub state changed; preview and approve again.');
116
+ await recheckAuthority(ctx, machine);
117
+ const result = await submitIssue(latest.checked.issue, preview.digest, options);
118
+ return safeResult(result.status === 'created' ? { status: 'created', data: { url: result.url } } : { ...result });
119
+ }
120
+ catch (error) {
121
+ return errorResult(error);
122
+ }
123
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "version": 1,
3
+ "repo": "example/demo",
4
+ "operation": "label-create",
5
+ "name": "bug",
6
+ "color": "d73a4a",
7
+ "description": "不具合"
8
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": 1,
3
+ "repo": "example/demo",
4
+ "operation": "label-delete",
5
+ "name": "obsolete"
6
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "version": 1,
3
+ "repo": "example/demo",
4
+ "operation": "label-edit",
5
+ "name": "bug",
6
+ "newName": "不具合",
7
+ "color": "d73a4a",
8
+ "description": "確認済みの不具合"
9
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "version": 1,
3
+ "repo": "example/demo",
4
+ "operation": "issue-labels",
5
+ "issue": 123,
6
+ "add": [
7
+ "bug"
8
+ ],
9
+ "remove": [
10
+ "needs-triage"
11
+ ]
12
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "version": 1,
3
+ "template": "parent",
4
+ "repo": "example/demo",
5
+ "title": "検索機能の仕様",
6
+ "fields": {
7
+ "purpose": "商品を検索できるようにする",
8
+ "specification": "入力語に一致する商品を一覧表示する",
9
+ "basicDesign": "既存の検索APIを利用。新しい基本設計は不要。",
10
+ "outOfScope": [
11
+ "全文検索エンジンの導入"
12
+ ],
13
+ "tasks": [
14
+ "子タスクを作成後、リンクを記録する"
15
+ ],
16
+ "completionCriteria": [
17
+ "仕様に沿った結果が表示され、独立検証が合格する"
18
+ ]
19
+ },
20
+ "agents": {
21
+ "coding-manager": {
22
+ "model": "openai-codex/gpt-6.1-sol",
23
+ "thinking": "medium",
24
+ "reason": "基本構成"
25
+ },
26
+ "coder": {
27
+ "model": "openai-codex/gpt-6-luna",
28
+ "thinking": "xhigh",
29
+ "reason": "基本構成"
30
+ },
31
+ "tester": {
32
+ "model": "openai-codex/gpt-6-luna",
33
+ "thinking": "xhigh",
34
+ "reason": "独立検証"
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,51 @@
1
+ {
2
+ "version": 1,
3
+ "template": "task",
4
+ "repo": "example/demo",
5
+ "title": "検索画面を実装する",
6
+ "parentIssue": 10,
7
+ "fields": {
8
+ "purpose": "商品検索画面を追加する",
9
+ "scope": [
10
+ "検索入力と結果一覧"
11
+ ],
12
+ "outOfScope": [
13
+ "検索APIの変更"
14
+ ],
15
+ "references": [
16
+ "親Issue #10の承認済み仕様"
17
+ ],
18
+ "dependencies": [
19
+ "なし:既存APIを利用"
20
+ ],
21
+ "executionOrder": "入力部と一覧部は別領域で並列作業可能。統合後に検証する。",
22
+ "acceptanceCriteria": [
23
+ {
24
+ "id": "C1",
25
+ "requirement": "一致する商品を表示する",
26
+ "verification": "テスト環境で検索入力を行う",
27
+ "expectedResult": "一致する商品だけ表示される"
28
+ }
29
+ ],
30
+ "estimate": "初期見積もり30〜90分、難易度中。保証ではない。",
31
+ "risk": "境界条件で結果表示が崩れる可能性",
32
+ "cost": "未算定:実行時のモデル料金・消費量が未確定"
33
+ },
34
+ "agents": {
35
+ "coding-manager": {
36
+ "model": "openai-codex/gpt-6.1-sol",
37
+ "thinking": "medium",
38
+ "reason": "基本構成"
39
+ },
40
+ "coder": {
41
+ "model": "openai-codex/gpt-6-luna",
42
+ "thinking": "xhigh",
43
+ "reason": "基本構成"
44
+ },
45
+ "tester": {
46
+ "model": "openai-codex/gpt-6-luna",
47
+ "thinking": "xhigh",
48
+ "reason": "独立検証"
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,41 @@
1
+ import type {ExtensionContext} from '@earendil-works/pi-coding-agent';
2
+ import {ScrollView,Text,Key,matchesKey,wrapTextWithAnsi,truncateToWidth,type Component} from '@earendil-works/pi-tui';
3
+ import type {ApprovalView} from '../src/service-types.js';
4
+ type Scope={signal:AbortSignal;isCurrent:()=>boolean};
5
+ function plain(text:string){return text.replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069]/g,c=>'\\u'+c.charCodeAt(0).toString(16).padStart(4,'0'));}
6
+ export async function showApproval(view:ApprovalView,scope:Scope,ui:ExtensionContext['ui']):Promise<boolean>{
7
+ if(scope.signal.aborted||!scope.isCurrent())return false;
8
+ try{
9
+ return await ui.custom<boolean>((tui,theme,_keys,done)=>{
10
+ let selected=0,settled=false;const text=plain(view.text),scroll=new ScrollView(new Text(text,0,0),{scrollbar:'hidden',overscroll:'contain'});
11
+ let cachedWidth=0,cachedLines:string[]=[];
12
+ const finish=(value:boolean)=>{if(settled)return;settled=true;done(value&&scope.isCurrent()&&!scope.signal.aborted);};
13
+ const abort=()=>finish(false);scope.signal.addEventListener('abort',abort,{once:true});
14
+ if(scope.signal.aborted||!scope.isCurrent())queueMicrotask(abort);
15
+ const component:Component&{dispose:()=>void}={
16
+ render(width){
17
+ const rows=tui.terminal.rows;if(width<16||rows<9||!scope.isCurrent()){queueMicrotask(abort);return [truncateToWidth('Approval unavailable. Cancelled.',width)];}
18
+ const viewport=Math.max(1,rows-7);if(cachedWidth!==width){cachedLines=wrapTextWithAnsi(text,Math.max(1,width));cachedWidth=width;}const lines=cachedLines;
19
+ scroll.updateLayout(lines.length,viewport,()=>tui.requestRender());
20
+ const top=scroll.scrollTop,visible=lines.slice(top,top+viewport);
21
+ return [theme.fg('accent',truncateToWidth('pi-gh review: '+view.operation,width)),truncateToWidth('Target and content below. No changes yet.',width),'',...visible,'',truncateToWidth((selected===0?'[Cancel] Approve':'Cancel [Approve]')+' '+(top+1)+'/'+lines.length,width),truncateToWidth('Up/Down/PgUp/PgDn scroll; Left/Right choose; Enter; Esc cancel',width)];
22
+ },
23
+ handleInput(data){
24
+ if(settled)return;if(!scope.isCurrent()||scope.signal.aborted)return finish(false);
25
+ if(matchesKey(data,Key.escape)||matchesKey(data,Key.ctrl('c')))return finish(false);
26
+ if(matchesKey(data,Key.left)||matchesKey(data,Key.right)||matchesKey(data,Key.tab))selected=selected===0?1:0;
27
+ else if(matchesKey(data,Key.enter))return finish(selected===1);
28
+ else if(matchesKey(data,Key.up))scroll.scrollBy(-1);
29
+ else if(matchesKey(data,Key.down))scroll.scrollBy(1);
30
+ else if(matchesKey(data,Key.pageUp))scroll.scrollBy(-Math.max(1,tui.terminal.rows-7));
31
+ else if(matchesKey(data,Key.pageDown))scroll.scrollBy(Math.max(1,tui.terminal.rows-7));
32
+ else if(matchesKey(data,Key.home))scroll.scrollToStart();
33
+ else if(matchesKey(data,Key.end))scroll.scrollToEnd();
34
+ tui.requestRender();
35
+ },
36
+ invalidate(){cachedWidth=0;scroll.invalidate();},
37
+ dispose(){scope.signal.removeEventListener('abort',abort);settled=true;}
38
+ };return component;
39
+ });
40
+ }catch{return false;}
41
+ }
@@ -0,0 +1,12 @@
1
+ import type {ExtensionAPI} from '@earendil-works/pi-coding-agent';
2
+ import {fileURLToPath} from 'node:url';
3
+ import {RuntimeScope} from './runtime-scope.ts';
4
+ import {registerTools} from './tools.ts';
5
+ export default function(pi:ExtensionAPI){
6
+ const scope=new RuntimeScope();registerTools(pi,scope,fileURLToPath(new URL('../',import.meta.url)));
7
+ pi.on('session_start',()=>scope.invalidate());
8
+ pi.on('session_before_switch',()=>scope.invalidate());
9
+ pi.on('session_before_fork',()=>scope.invalidate());
10
+ pi.on('session_before_tree',()=>scope.invalidate());
11
+ pi.on('session_shutdown',()=>scope.invalidate());
12
+ }
@@ -0,0 +1,15 @@
1
+ export class RuntimeScope {
2
+ private generation=0;private leases=new Set<AbortController>();private tail:Promise<unknown>=Promise.resolve();
3
+ get stamp(){return String(this.generation);}
4
+ isCurrent(stamp:string){return stamp===this.stamp;}
5
+ acquire(caller?:AbortSignal){
6
+ const controller=new AbortController(),stamp=this.stamp;this.leases.add(controller);
7
+ const abort=()=>controller.abort();caller?.addEventListener('abort',abort,{once:true});if(caller?.aborted)abort();
8
+ let disposed=false;return {stamp,signal:controller.signal,dispose:()=>{if(disposed)return;disposed=true;caller?.removeEventListener('abort',abort);this.leases.delete(controller);}};
9
+ }
10
+ invalidate(){this.generation++;for(const controller of this.leases)controller.abort();this.leases.clear();}
11
+ runExclusive<T>(fn:()=>Promise<T>):Promise<T>{
12
+ const stamp=this.stamp;const result=this.tail.then(()=>{if(!this.isCurrent(stamp))throw new Error('Session changed; queued operation expired.');return fn();});
13
+ this.tail=result.then(()=>undefined,()=>undefined);return result;
14
+ }
15
+ }
@@ -0,0 +1,53 @@
1
+ import {Type} from '@earendil-works/pi-ai';
2
+ import type {ExtensionAPI,ExtensionToolContext} from '@earendil-works/pi-coding-agent';
3
+ import {runOperation} from '../dist/src/service.js';
4
+ import {safeResult,errorResult} from '../dist/src/result.js';
5
+ import {loadPermissions,permissionPath} from '../dist/src/permissions.js';
6
+ import {runGithubOperation} from '../dist/src/operations.js';
7
+ import {githubReads,githubWrites} from '../dist/src/operation-input.js';
8
+ import type {GithubOperation,GithubArgs} from '../src/operation-input.js';
9
+ import type {Operation,IssueArgs,LabelsArgs,FormArgs,OperationResult} from '../src/service-types.js';
10
+ import {showApproval} from './approval-ui.ts';
11
+ import {RuntimeScope} from './runtime-scope.ts';
12
+ const template={template:Type.Optional(Type.Union([Type.Literal('parent'),Type.Literal('task')])),templatePath:Type.Optional(Type.String({minLength:1}))};
13
+ const issue=Type.Object({draftPath:Type.String({minLength:1}),...template},{additionalProperties:false});
14
+ const labels=Type.Object({changePath:Type.String({minLength:1})},{additionalProperties:false});
15
+ const form=Type.Object(template,{additionalProperties:false});
16
+ const output=Type.Object({status:Type.String(),data:Type.Optional(Type.Unknown()),problems:Type.Optional(Type.Array(Type.Object({code:Type.String(),path:Type.String(),message:Type.String()}))),message:Type.Optional(Type.String()),candidateUrl:Type.Optional(Type.String())},{additionalProperties:false});
17
+ const operations:Array<Operation|GithubOperation>=['gh_issue_validate','gh_issue_preview','gh_issue_submit','gh_labels_validate','gh_labels_preview','gh_labels_apply','gh_issue_form',...githubReads,...githubWrites];
18
+ const repo=Type.String({minLength:3});
19
+ const issueRead=Type.Object({repo,issue:Type.Integer({minimum:1})},{additionalProperties:false});
20
+ const issueList=Type.Object({repo,state:Type.Optional(Type.Union([Type.Literal('open'),Type.Literal('closed'),Type.Literal('all')]))},{additionalProperties:false});
21
+ const projectRead=Type.Object({projectId:Type.String({pattern:'^PVT_[A-Za-z0-9_-]+$'})},{additionalProperties:false});
22
+ function identity(ctx:ExtensionToolContext){return ctx.sessionManager.getSessionId()+':'+ctx.sessionManager.getLeafId();}
23
+ function render(result:OperationResult){
24
+ const safe=safeResult(result),full=JSON.stringify(safe),limit=32768;
25
+ return {content:[{type:'text' as const,text:full.length<=limit?full:full.slice(0,limit)+'\n[truncated; complete masked data in structuredContent]'}],details:safe,structuredContent:safe as never,isError:['rejected','not-started','unknown'].includes(safe.status)};
26
+ }
27
+ export function registerTools(pi:ExtensionAPI,scope:RuntimeScope,packageRoot:string){
28
+ pi.registerTool({name:'gh_capabilities',label:'GitHub capabilities',description:'Return pi-gh tool contract version and supported operations. No remote access.',parameters:Type.Object({},{additionalProperties:false}),outputSchema:output,annotations:{readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false},async execute(){return render({status:'read',data:{contractVersion:1,operations:[...operations,'gh_capabilities']}});}});
29
+ for(const name of operations){
30
+ const write=name==='gh_issue_submit'||name==='gh_labels_apply'||(githubWrites as readonly string[]).includes(name);
31
+ pi.registerTool({
32
+ name,label:name,description:write?'Preview and apply one GitHub change after local human approval or an exact owner-configured permission grant. Never auto-retries unknown outcomes.':'Validate or preview a GitHub workflow. Preview may read remote metadata; never changes GitHub.',
33
+ parameters:(githubWrites as readonly string[]).includes(name)?labels:(githubReads as readonly string[]).includes(name)?(name==='gh_issue_list'?issueList:name.startsWith('gh_project_')?projectRead:issueRead):name.startsWith('gh_labels_')?labels:name==='gh_issue_form'?form:issue,outputSchema:output,
34
+ exposure:'direct',executionMode:write?'sequential':'parallel',
35
+ annotations:{readOnlyHint:!write,destructiveHint:write,idempotentHint:!write,openWorldHint:name!=='gh_issue_validate'&&name!=='gh_labels_validate'&&name!=='gh_issue_form'},
36
+ async execute(_id,args,signal,_update,ctx){
37
+ const captured=identity(ctx),lease=scope.acquire(signal),isCurrent=()=>scope.isCurrent(lease.stamp)&&identity(ctx)===captured;
38
+ const work=async()=>{
39
+ const child=!!process.env.PI_SUBAGENT_CHILD,headless=ctx.mode!=='tui'||!ctx.hasUI;
40
+ const permissions=write?await loadPermissions(permissionPath(),{child,headless}):undefined;
41
+ const context={
42
+ cwd:ctx.cwd,packageRoot,interactive:!headless&&!child,permissions,signal:lease.signal,stamp:lease.stamp,isCurrent,
43
+ confirm:(view:import('../src/service-types.js').ApprovalView)=>showApproval(view,{signal:lease.signal,isCurrent},ctx.ui)
44
+ };
45
+ return (githubReads as readonly string[]).includes(name)||(githubWrites as readonly string[]).includes(name)?runGithubOperation(name as GithubOperation,args as GithubArgs,context):runOperation(name as Operation,args as IssueArgs|LabelsArgs|FormArgs,context);
46
+ };
47
+ try{return render(write?await scope.runExclusive(work):await work());}
48
+ catch(error){return render(errorResult(error));}
49
+ finally{lease.dispose();}
50
+ }
51
+ });
52
+ }
53
+ }
package/package.json CHANGED
@@ -1,6 +1,53 @@
1
1
  {
2
2
  "name": "@papillon6814/pi-gh",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Prepare and approve GitHub issue workflows inside Pi",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "engines": {
8
+ "node": ">=22.19.0"
9
+ },
10
+ "keywords": [
11
+ "pi-package"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/KendrickMalar/pi-gh.git"
16
+ },
17
+ "pi": {
18
+ "extensions": [
19
+ "extensions/index.ts"
20
+ ]
21
+ },
22
+ "files": [
23
+ "extensions/*.ts",
24
+ "dist/src",
25
+ "templates",
26
+ "examples",
27
+ "README.md",
28
+ "LICENSE"
29
+ ],
30
+ "scripts": {
31
+ "build": "tsc -p tsconfig.core.json",
32
+ "build:test": "tsc -p tsconfig.test.json",
33
+ "typecheck": "npm run build && tsc --noEmit -p tsconfig.test.json && tsc --noEmit -p tsconfig.extension.json",
34
+ "test:extension": "npm run build && node --test test/*.test.mjs",
35
+ "test:core": "npm run build && npm run build:test && node --test dist/test/core/*.test.js",
36
+ "test:service": "npm run build && npm run build:test && node --test dist/test/service*.test.js",
37
+ "test": "npm run build && npm run build:test && node --test dist/test/core/*.test.js dist/test/service*.test.js dist/test/package.test.js test/*.test.mjs"
38
+ },
39
+ "peerDependencies": {
40
+ "@earendil-works/pi-ai": "*",
41
+ "@earendil-works/pi-coding-agent": "*",
42
+ "@earendil-works/pi-tui": "*"
43
+ },
44
+ "dependencies": {
45
+ "yaml": "^2.8.1"
46
+ },
47
+ "devDependencies": {
48
+ "@earendil-works/pi-coding-agent": "1.0.4",
49
+ "@types/node": "^22.15.30",
50
+ "jiti": "2.6.1",
51
+ "typescript": "^5.9.2"
52
+ }
53
+ }
@@ -0,0 +1,47 @@
1
+ version: 1
2
+ id: parent
3
+ name: Parent specification
4
+ description: 親Issue:仕様・基本設計とタスク管理
5
+ kind: parent
6
+ modelPolicy: policies/models.yml
7
+ agents:
8
+ - coding-manager
9
+ - coder
10
+ - tester
11
+ fields:
12
+ - id: purpose
13
+ label: 目的
14
+ type: text
15
+ required: true
16
+ description: 内容を記載。不要・未算定なら理由を記載
17
+ maxLength: 20000
18
+ - id: specification
19
+ label: 仕様
20
+ type: text
21
+ required: true
22
+ description: 内容を記載。不要・未算定なら理由を記載
23
+ maxLength: 20000
24
+ - id: basicDesign
25
+ label: 基本設計
26
+ type: text
27
+ required: true
28
+ description: 内容を記載。不要・未算定なら理由を記載
29
+ maxLength: 20000
30
+ - id: outOfScope
31
+ label: 対象外
32
+ type: string-list
33
+ required: true
34
+ description: 文字列の配列。対象がなければ理由付きで「なし」を記載
35
+ minItems: 1
36
+ - id: tasks
37
+ label: タスク一覧
38
+ type: string-list
39
+ required: true
40
+ description: 文字列の配列。対象がなければ理由付きで「なし」を記載
41
+ minItems: 1
42
+ - id: completionCriteria
43
+ label: 全体の完了条件
44
+ type: string-list
45
+ required: true
46
+ description: 文字列の配列。対象がなければ理由付きで「なし」を記載
47
+ minItems: 1
@@ -0,0 +1,20 @@
1
+ version: 1
2
+ agents:
3
+ coding-manager:
4
+ - model: openai-codex/gpt-6.1-sol
5
+ thinking: medium
6
+ tier: basic
7
+ coder:
8
+ - model: openai-codex/gpt-6-luna
9
+ thinking: xhigh
10
+ tier: basic
11
+ - model: openai-codex/gpt-6.1-sol
12
+ thinking: medium
13
+ tier: upper
14
+ tester:
15
+ - model: openai-codex/gpt-6-luna
16
+ thinking: xhigh
17
+ tier: basic
18
+ - model: openai-codex/gpt-6.1-sol
19
+ thinking: medium
20
+ tier: upper
@@ -0,0 +1,71 @@
1
+ version: 1
2
+ id: task
3
+ name: Implementation task
4
+ description: タスクIssue:承認済み仕様に基づく実装と検証
5
+ kind: task
6
+ modelPolicy: policies/models.yml
7
+ agents:
8
+ - coding-manager
9
+ - coder
10
+ - tester
11
+ fields:
12
+ - id: purpose
13
+ label: 目的
14
+ type: text
15
+ required: true
16
+ description: 内容を記載。不要・未算定なら理由を記載
17
+ maxLength: 20000
18
+ - id: scope
19
+ label: 実装範囲
20
+ type: string-list
21
+ required: true
22
+ description: 文字列の配列。対象がなければ理由付きで「なし」を記載
23
+ minItems: 1
24
+ - id: outOfScope
25
+ label: 対象外
26
+ type: string-list
27
+ required: true
28
+ description: 文字列の配列。対象がなければ理由付きで「なし」を記載
29
+ minItems: 1
30
+ - id: references
31
+ label: 関連資料
32
+ type: string-list
33
+ required: true
34
+ description: 文字列の配列。対象がなければ理由付きで「なし」を記載
35
+ minItems: 1
36
+ - id: dependencies
37
+ label: 依存Issue
38
+ type: string-list
39
+ required: true
40
+ description: 文字列の配列。対象がなければ理由付きで「なし」を記載
41
+ minItems: 1
42
+ - id: executionOrder
43
+ label: 着手順序・並列範囲
44
+ type: text
45
+ required: true
46
+ description: 内容を記載。不要・未算定なら理由を記載
47
+ maxLength: 20000
48
+ - id: acceptanceCriteria
49
+ label: 受け入れ要件
50
+ type: criteria
51
+ required: true
52
+ description: id・requirement・verification・expectedResultを持つ配列
53
+ minItems: 1
54
+ - id: estimate
55
+ label: 工数・難易度
56
+ type: text
57
+ required: true
58
+ description: 内容を記載。不要・未算定なら理由を記載
59
+ maxLength: 20000
60
+ - id: risk
61
+ label: リスク
62
+ type: text
63
+ required: true
64
+ description: 内容を記載。不要・未算定なら理由を記載
65
+ maxLength: 20000
66
+ - id: cost
67
+ label: 費用と算定前提
68
+ type: text
69
+ required: true
70
+ description: 内容を記載。不要・未算定なら理由を記載
71
+ maxLength: 20000