chief-clancy 0.7.1 → 0.7.3

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/README.md CHANGED
@@ -143,6 +143,9 @@ npx chief-clancy
143
143
 
144
144
  # 6. Go AFK
145
145
  /clancy:run
146
+
147
+ # Fully autonomous (no prompts at any step):
148
+ # /clancy:brief --afk #42 → /clancy:approve-brief --afk → /clancy:run
146
149
  ```
147
150
 
148
151
  ---
@@ -42,8 +42,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
42
42
  `;su(s,d,"utf8");return}let a=o!=null?` | pr:${o}`:"",u=i?` | parent:${i}`:"",l=`${c} | ${t} | ${r} | ${n}${a}${u}
43
43
  `;su(s,l,"utf8")}function au(e){let t=cu(e,".clancy","progress.txt"),r;try{r=Fm(t,"utf8")}catch{return[]}let n=[];for(let o of r.split(`
44
44
  `)){let i=o.trim();if(!i)continue;let s=i.split(" | ");if(s.length<4)continue;let c=s[0];if(s[1]==="BRIEF"||s[1]==="APPROVE_BRIEF"){n.push({timestamp:c,key:s[2],summary:s.slice(3).join(" | "),status:s[1]});continue}let a=s[1],u,l,d,m=[];for(let h=2;h<s.length;h++){let y=s[h],_=y.match(/^pr:(\d+)$/),z=y.match(/^parent:(.+)$/);_?l=parseInt(_[1],10):z?d=z[1]:h>=3&&!u&&y===y.toUpperCase()&&y.length>1?u=y:m.push(y)}u&&(u==="APPROVE"&&(u="APPROVE_PLAN"),n.push({timestamp:c,key:a,summary:m.join(" | "),status:u,...l!=null&&{prNumber:l},...d!=null&&{parent:d}}))}return n}function uu(e,t){let r=au(e),n=t.toLowerCase(),o=0;for(let i of r)i.key.toLowerCase()===n&&i.status==="REWORK"&&o++;return o}function U(e,t){let r=au(e),n=new Map;for(let o of r)n.set(o.key,o);return[...n.values()].filter(o=>o.status===t)}function Ym(e){return e.startsWith("epic/")||e.startsWith("milestone/")}function dr(e,t,r,n){let o=[],i=r?Ym(r):!1;switch(e.provider){case"github":o.push(i?`Part of ${t.key}`:`Closes ${t.key}`);break;case"jira":o.push(`**Jira:** [${t.key}](${e.env.JIRA_BASE_URL}/browse/${t.key})`);break;case"linear":o.push(`**Linear:** ${t.key}`);break}return o.push(""),t.description&&(o.push("## Description"),o.push(""),o.push(t.description),o.push("")),n&&(o.push("## \u26A0 Verification Warning"),o.push(""),o.push(n),o.push(""),o.push("This PR may need manual fixes before merging."),o.push("")),o.push("---"),o.push("*Created by [Clancy](https://github.com/Pushedskydiver/clancy)*"),o.push(""),o.push("---"),o.push("<details>"),o.push("<summary><strong>Rework instructions</strong> (click to expand)</summary>"),o.push(""),o.push("To request changes:"),o.push("- **Code comments** \u2014 leave inline comments on specific lines. These are always picked up automatically."),o.push("- **General feedback** \u2014 reply with a comment starting with `Rework:` followed by what needs fixing. Comments without the `Rework:` prefix are treated as discussion."),o.push(""),o.push("Example: `Rework: The form validation doesn't handle empty passwords`"),o.push(""),o.push("</details>"),o.join(`
45
- `)}function lu(e,t,r){let n=[];n.push(`## ${e} \u2014 ${t}`),n.push(""),n.push("### Children"),n.push("");for(let o of r){let i=o.prNumber?` (#${o.prNumber})`:"";n.push(`- ${o.key} \u2014 ${o.summary}${i}`)}return n.push(""),n.push("---"),n.push("*Created by [Clancy](https://github.com/Pushedskydiver/clancy)*"),n.join(`
46
- `)}import{execFileSync as Gm}from"node:child_process";function pu(e){let t=e.trim().replace(/\.git$/,""),r=t.match(/^git@([^:]+):(.+)$/),n=t.match(/^(?:https?|ssh):\/\/(?:[^@]+@)?([^/:]+)(?::\d+)?\/(.+)$/),o=r?.[1]??n?.[1],i=r?.[2]??n?.[2];if(!(!o||!i))return{hostname:o,path:i}}function qm(e){let t=pu(e);if(!t)return{host:"unknown",url:e};let{hostname:r,path:n}=t;switch(Hm(r)){case"github":{let i=n.split("/");return i.length>=2?{host:"github",owner:i[0],repo:i[1],hostname:r}:{host:"unknown",url:e}}case"gitlab":return{host:"gitlab",projectPath:n,hostname:r};case"bitbucket":{let i=n.match(/^scm\/([^/]+)\/(.+)$/);if(i)return{host:"bitbucket-server",projectKey:i[1],repoSlug:i[2],hostname:r};let s=n.split("/");return s.length>=2?{host:"bitbucket",workspace:s[0],repoSlug:s[1],hostname:r}:{host:"unknown",url:e}}case"azure":return{host:"azure",url:e};default:return{host:"unknown",url:e}}}function Hm(e){let t=e.toLowerCase();return t==="github.com"||t.includes("github")?"github":t==="gitlab.com"||t.includes("gitlab")?"gitlab":t==="bitbucket.org"||t.includes("bitbucket")?"bitbucket":t.includes("dev.azure")||t.includes("visualstudio")?"azure":"unknown"}function ye(e){let t;try{t=Gm("git",["remote","get-url","origin"],{encoding:"utf8"}).trim()}catch{return{host:"none"}}return t?e?Vm(t,e):qm(t):{host:"none"}}function Vm(e,t){let r=pu(e);if(!r)return{host:"unknown",url:e};let{hostname:n,path:o}=r;switch(t.toLowerCase()){case"github":{let i=o.split("/");return i.length>=2?{host:"github",owner:i[0],repo:i[1],hostname:n}:{host:"unknown",url:e}}case"gitlab":return{host:"gitlab",projectPath:o,hostname:n};case"bitbucket":{let i=o.split("/");return i.length>=2?{host:"bitbucket",workspace:i[0],repoSlug:i[1],hostname:n}:{host:"unknown",url:e}}case"bitbucket-server":{let i=o.match(/^scm\/([^/]+)\/(.+)$/);if(i)return{host:"bitbucket-server",projectKey:i[1],repoSlug:i[2],hostname:n};let s=o.split("/");return s.length>=2?{host:"bitbucket-server",projectKey:s[0],repoSlug:s[1],hostname:n}:{host:"unknown",url:e}}default:return{host:"unknown",url:e}}}function _t(e,t){if(t)return t.replace(/\/$/,"");switch(e.host){case"github":return e.hostname==="github.com"?"https://api.github.com":`https://${e.hostname}/api/v3`;case"gitlab":return`https://${e.hostname}/api/v4`;case"bitbucket":return"https://api.bitbucket.org/2.0";case"bitbucket-server":return`https://${e.hostname}/rest/api/1.0`;default:return}}var Wm=E({id:_e(),links:E({html:$(E({href:$(b())}))}),participants:D(E({state:$(b()),role:b()}))}),fu=E({values:D(Wm)}),Xm=E({content:E({raw:b()}),inline:$(E({path:$(b())})),created_on:b(),user:$(E({nickname:$(b())}))}),Qs=E({values:D(Xm)}),Qm=E({id:_e(),links:E({self:$(D(E({href:$(b())})))}),reviewers:D(E({status:b()}))}),du=E({values:D(Qm)}),eh=E({text:b(),anchor:$(E({path:$(b())})),createdDate:_e(),author:$(E({slug:$(b())}))}),th=E({action:b(),comment:$(eh)}),ec=E({values:D(th)});async function Se(e,t,r,n,o){let i=new AbortController,s=setTimeout(()=>i.abort(),3e4);try{let c=await fetch(e,{method:"POST",headers:{...t,"Content-Type":"application/json"},body:JSON.stringify(r),signal:i.signal});if(!c.ok){let l=await c.text().catch(()=>""),d=o?.(c.status,l)??!1;return{ok:!1,error:`HTTP ${c.status}${l?`: ${l.slice(0,200)}`:""}`,alreadyExists:d}}let a=await c.json(),u=n(a);return!u.url&&!u.number?{ok:!1,error:"PR created but response missing URL and number"}:{ok:!0,url:u.url,number:u.number}}catch(c){return{ok:!1,error:c instanceof Error?c.message:String(c)}}finally{clearTimeout(s)}}function Ue(e,t){return`Basic ${Buffer.from(`${e}:${t}`).toString("base64")}`}function te(e){return e.trim().toLowerCase().startsWith("rework:")}function Pe(e){return e.trim().replace(/^rework:\s*/i,"").trim()}async function mu(e,t,r,n,o,i,s,c){return Se(`https://api.bitbucket.org/2.0/repositories/${r}/${n}/pullrequests`,{Authorization:Ue(e,t)},{title:s,description:c,source:{branch:{name:o}},destination:{branch:{name:i}},close_source_branch:!0},a=>{let u=a;return{url:u.links?.html?.href??"",number:u.id??0}},(a,u)=>a===409&&u.includes("already exists"))}async function hu(e,t,r,n,o,i,s,c){return Se(`${t}/projects/${r}/repos/${n}/pull-requests`,{Authorization:`Bearer ${e}`},{title:s,description:c,fromRef:{id:`refs/heads/${o}`,repository:{slug:n,project:{key:r}}},toRef:{id:`refs/heads/${i}`,repository:{slug:n,project:{key:r}}}},a=>{let u=a;return{url:u.links?.self?.[0]?.href??"",number:u.id??0}},(a,u)=>a===409&&(u.includes("already exists")||u.includes("Only one pull request")))}async function gu(e,t,r,n,o,i){try{return(await fetch(`https://api.bitbucket.org/2.0/repositories/${r}/${n}/pullrequests/${o}/comments`,{method:"POST",headers:{Authorization:Ue(e,t),"Content-Type":"application/json"},body:JSON.stringify({content:{raw:i}})})).ok}catch{return!1}}async function _u(e,t,r,n,o,i){try{return(await fetch(`${t}/projects/${r}/repos/${n}/pull-requests/${o}/comments`,{method:"POST",headers:{Authorization:`Bearer ${e}`,"Content-Type":"application/json"},body:JSON.stringify({text:i})})).ok}catch{return!1}}async function yu(e,t,r,n,o,i){try{let s=`https://api.bitbucket.org/2.0/repositories/${r}/${n}/pullrequests?q=source.branch.name="${o}"&state=OPEN`,c=await fetch(s,{headers:{Authorization:Ue(e,t)}});if(!c.ok)return;let a=fu.parse(await c.json());if(a.values.length===0)return;let u=a.values[0],l=u.links.html?.href??"",d=`https://api.bitbucket.org/2.0/repositories/${r}/${n}/pullrequests/${u.id}/comments?pagelen=100`,m=await fetch(d,{headers:{Authorization:Ue(e,t)}});if(!m.ok)return;let h=Qs.parse(await m.json()),y=i?h.values.filter(H=>H.created_on>i):h.values,_=y.some(H=>H.inline!=null),z=y.some(H=>H.inline==null&&te(H.content.raw));return{changesRequested:_||z,prNumber:u.id,prUrl:l}}catch{return}}async function bu(e,t,r,n,o,i){try{let s=`https://api.bitbucket.org/2.0/repositories/${r}/${n}/pullrequests/${o}/comments?pagelen=100`,c=await fetch(s,{headers:{Authorization:Ue(e,t)}});if(!c.ok)return[];let a=Qs.parse(await c.json()),u=i?a.values.filter(d=>d.created_on>i):a.values,l=[];for(let d of u)if(d.inline!=null){let m=d.inline.path?`[${d.inline.path}] `:"";l.push(`${m}${d.content.raw}`)}else te(d.content.raw)&&l.push(Pe(d.content.raw));return l}catch{return[]}}async function $u(e,t,r,n,o,i){try{let s=`${t}/projects/${r}/repos/${n}/pull-requests?state=OPEN&at=refs/heads/${o}`,c=await fetch(s,{headers:{Authorization:`Bearer ${e}`}});if(!c.ok)return;let a=du.parse(await c.json());if(a.values.length===0)return;let u=a.values[0],l=u.links.self?.[0]?.href??"",d=`${t}/projects/${r}/repos/${n}/pull-requests/${u.id}/activities?limit=100`,m=await fetch(d,{headers:{Authorization:`Bearer ${e}`}});if(!m.ok)return;let h=ec.parse(await m.json()),y=i?Date.parse(i):void 0,_=h.values.filter(R=>R.action==="COMMENTED"&&R.comment&&(y==null||R.comment.createdDate>y)),z=_.some(R=>R.comment.anchor!=null),F=_.some(R=>R.comment.anchor==null&&te(R.comment.text));return{changesRequested:z||F,prNumber:u.id,prUrl:l}}catch{return}}async function xu(e,t,r,n,o,i){try{let s=`${t}/projects/${r}/repos/${n}/pull-requests/${o}/activities?limit=100`,c=await fetch(s,{headers:{Authorization:`Bearer ${e}`}});if(!c.ok)return[];let a=ec.parse(await c.json()),u=i?Date.parse(i):void 0,l=[];for(let d of a.values){if(d.action!=="COMMENTED"||!d.comment||u!=null&&d.comment.createdDate<=u)continue;let m=d.comment;if(m.anchor!=null){let h=m.anchor.path?`[${m.anchor.path}] `:"";l.push(`${h}${m.text}`)}else te(m.text)&&l.push(Pe(m.text))}return l}catch{return[]}}var rh=p.object({number:p.number(),title:p.string(),body:p.optional(p.nullable(p.string())),pull_request:p.optional(p.unknown()),milestone:p.optional(p.nullable(p.object({title:p.string()}))),labels:p.optional(p.array(p.object({name:p.optional(p.string())})))}),wu=p.array(rh),nh=p.object({id:p.number(),body:p.optional(p.nullable(p.string())),created_at:p.string(),user:p.optional(p.object({login:p.string()}))}),tc=p.array(nh),oh=p.object({number:p.number(),html_url:p.string(),state:p.string()}),vu=p.array(oh),ih=p.object({state:p.string(),user:p.object({login:p.string()}),submitted_at:p.string()}),ku=p.array(ih),sh=p.object({body:p.optional(p.nullable(p.string())),path:p.optional(p.string()),created_at:p.optional(p.string()),user:p.optional(p.object({login:p.string()}))}),rc=p.array(sh);var q="https://api.github.com";async function mr(e,t,r,n){try{let o=new AbortController,i=setTimeout(()=>o.abort(),1e4),s=await fetch(e,{headers:t,signal:o.signal});if(clearTimeout(i),s.ok)return{ok:!0};let c=r[s.status];return c?{ok:!1,error:c}:{ok:!1,error:`\u2717 HTTP ${s.status}`}}catch{return{ok:!1,error:n}}}function Q(e){return{Authorization:`Bearer ${e}`,Accept:"application/vnd.github+json","X-GitHub-Api-Version":"2022-11-28"}}function be(e){return{Authorization:`Basic ${e}`,Accept:"application/json"}}async function zu(e,t,r,n,o=q,i,s){try{let c=Q(e),a=await fetch(`${o}/repos/${t}/pulls?head=${n}:${r}&state=open`,{headers:c});if(!a.ok)return;let u=vu.parse(await a.json());if(u.length===0)return;let l=u[0],d=i?`&since=${i}`:"",[m,h]=await Promise.all([fetch(`${o}/repos/${t}/pulls/${l.number}/comments?per_page=100${d}`,{headers:c}),fetch(`${o}/repos/${t}/issues/${l.number}/comments?per_page=100${d}`,{headers:c})]);if(!m.ok||!h.ok)return;let y=rc.parse(await m.json()),_=tc.parse(await h.json()),z=s?y.filter(re=>re.user?.login!==s):y,F=s?_.filter(re=>re.user?.login!==s):_,H=z.length>0,R=F.some(re=>re.body&&te(re.body)),O=H||R,J;if(!O)try{let re=await fetch(`${o}/repos/${t}/pulls/${l.number}/reviews?per_page=100`,{headers:c});if(re.ok){let Er=ku.parse(await re.json()),ac=new Map;for(let ie of Er)ie.state==="PENDING"||ie.state==="DISMISSED"||ac.set(ie.user.login,ie.state);let uc=[...ac.entries()].filter(([,ie])=>ie==="CHANGES_REQUESTED");uc.length>0&&(O=!0,J=uc.map(([ie])=>ie))}}catch{}return{changesRequested:O,prNumber:l.number,prUrl:l.html_url,reviewers:J}}catch{return}}async function Su(e,t,r,n=q,o,i){try{let s=Q(e),c=o?`&since=${o}`:"",[a,u]=await Promise.all([fetch(`${n}/repos/${t}/pulls/${r}/comments?per_page=100${c}`,{headers:s}),fetch(`${n}/repos/${t}/issues/${r}/comments?per_page=100${c}`,{headers:s})]);if(!a.ok||!u.ok)return[];let l=rc.parse(await a.json()),d=tc.parse(await u.json()),m=i?l.filter(_=>_.user?.login!==i):l,h=i?d.filter(_=>_.user?.login!==i):d,y=[];for(let _ of m){if(!_.body)continue;let z=_.path?`[${_.path}] `:"";y.push(`${z}${_.body}`)}for(let _ of h)_.body&&te(_.body)&&y.push(Pe(_.body));return y}catch{return[]}}async function Pu(e,t,r,n,o=q){try{return(await fetch(`${o}/repos/${t}/issues/${r}/comments`,{method:"POST",headers:{...Q(e),"Content-Type":"application/json"},body:JSON.stringify({body:n})})).ok}catch{return!1}}async function Eu(e,t,r,n,o=q){try{return(await fetch(`${o}/repos/${t}/pulls/${r}/requested_reviewers`,{method:"POST",headers:{...Q(e),"Content-Type":"application/json"},body:JSON.stringify({reviewers:n})})).ok}catch{return!1}}async function Iu(e,t,r,n,o,i,s=q){return Se(`${s}/repos/${t}/pulls`,Q(e),{title:o,head:r,base:n,body:i},c=>{let a=c;return{url:a.html_url??"",number:a.number??0}},(c,a)=>c===422&&a.includes("already exists"))}var ch=E({iid:_e(),web_url:b(),detailed_merge_status:$(b())}),Tu=D(ch),ah=E({body:b(),resolvable:De(),resolved:$(De()),system:De(),type:$(cr(b())),created_at:$(b()),position:$(E({new_path:$(b())})),author:$(E({username:b()}))}),uh=E({id:$(b()),notes:D(ah)}),nc=D(uh);async function Au(e,t,r,n,o,i,s){let c=encodeURIComponent(r);return Se(`${t}/projects/${c}/merge_requests`,{"PRIVATE-TOKEN":e},{source_branch:n,target_branch:o,title:i,description:s,remove_source_branch:!0},a=>{let u=a;return{url:u.web_url??"",number:u.iid??0}},(a,u)=>a===409&&u.includes("already exists"))}async function Ru(e,t,r,n,o){try{let i=encodeURIComponent(r);return(await fetch(`${t}/projects/${i}/merge_requests/${n}/notes`,{method:"POST",headers:{"PRIVATE-TOKEN":e,"Content-Type":"application/json"},body:JSON.stringify({body:o})})).ok}catch{return!1}}async function Cu(e,t,r,n,o){let i=encodeURIComponent(r),s=0;for(let c of o)try{(await fetch(`${t}/projects/${i}/merge_requests/${n}/discussions/${c}`,{method:"PUT",headers:{"PRIVATE-TOKEN":e,"Content-Type":"application/json"},body:JSON.stringify({resolved:!0})})).ok&&s++}catch{}return s}async function Zu(e,t,r,n,o){try{let i=encodeURIComponent(r),s=`${t}/projects/${i}/merge_requests?source_branch=${n}&state=opened`,c=await fetch(s,{headers:{"PRIVATE-TOKEN":e}});if(!c.ok)return;let a=Tu.parse(await c.json());if(a.length===0)return;let u=a[0],l=`${t}/projects/${i}/merge_requests/${u.iid}/discussions?per_page=100`,d=await fetch(l,{headers:{"PRIVATE-TOKEN":e}});if(!d.ok)return;let m=nc.parse(await d.json()),h=!1;for(let y of m){for(let _ of y.notes)if(!_.system&&!(o&&_.created_at&&_.created_at<=o)){if(_.type==="DiffNote"&&_.resolvable!==!1&&_.resolved!==!0){h=!0;break}if(te(_.body)){h=!0;break}}if(h)break}return{changesRequested:h,prNumber:u.iid,prUrl:u.web_url}}catch{return}}async function Nu(e,t,r,n,o){try{let i=encodeURIComponent(r),s=`${t}/projects/${i}/merge_requests/${n}/discussions?per_page=100`,c=await fetch(s,{headers:{"PRIVATE-TOKEN":e}});if(!c.ok)return{comments:[],discussionIds:[]};let a=nc.parse(await c.json()),u=[],l=[];for(let d of a){let m=!1;for(let h of d.notes)if(!h.system&&!(o&&h.created_at&&h.created_at<=o))if(h.type==="DiffNote"&&h.resolvable!==!1&&h.resolved!==!0){let y=h.position?.new_path?`[${h.position.new_path}] `:"";u.push(`${y}${h.body}`),m=!0}else te(h.body)&&(u.push(Pe(h.body)),m=!0);m&&d.id&&l.push(d.id)}return{comments:u,discussionIds:l}}catch{return{comments:[],discussionIds:[]}}}function yt(e,t){let r=L(e);switch(t.host){case"github":if(r.GITHUB_TOKEN)return{token:r.GITHUB_TOKEN};break;case"gitlab":if(r.GITLAB_TOKEN)return{token:r.GITLAB_TOKEN};break;case"bitbucket":if(r.BITBUCKET_USER&&r.BITBUCKET_TOKEN)return{token:r.BITBUCKET_TOKEN,username:r.BITBUCKET_USER};break;case"bitbucket-server":if(r.BITBUCKET_TOKEN)return{token:r.BITBUCKET_TOKEN};break}}async function bt(e,t,r,n,o,i){let s=yt(e,t);if(!s)return;let c=_t(t,L(e).CLANCY_GIT_API_URL);if(c)switch(t.host){case"github":return Iu(s.token,`${t.owner}/${t.repo}`,r,n,o,i,c);case"gitlab":return Au(s.token,c,t.projectPath,r,n,o,i);case"bitbucket":return mu(s.username,s.token,t.workspace,t.repoSlug,r,n,o,i);case"bitbucket-server":return hu(s.token,c,t.projectKey,t.repoSlug,r,n,o,i);default:return}}function hr(e,t,r){let n=encodeURIComponent(t),o=encodeURIComponent(r);if(e.host==="github")return`https://${e.hostname}/${e.owner}/${e.repo}/compare/${o}...${n}`;if(e.host==="gitlab")return`https://${e.hostname}/${e.projectPath}/-/merge_requests/new?merge_request[source_branch]=${n}&merge_request[target_branch]=${o}`;if(e.host==="bitbucket")return`https://${e.hostname}/${e.workspace}/${e.repoSlug}/pull-requests/new?source=${n}&dest=${o}`;if(e.host==="bitbucket-server")return`https://${e.hostname}/projects/${e.projectKey}/repos/${e.repoSlug}/pull-requests?create&sourceBranch=refs/heads/${n}&targetBranch=refs/heads/${o}`}function oc(e,t){let r=pc(e),n=Ke(e);if(r)return vt(e)?!0:(console.log(S(`\u26A0 Epic branch ${e} exists on remote but could not be fetched.`)),!1);if(n)return console.log(Te(`\u2717 Epic branch ${e} exists locally but not on remote.`)),console.log(S(" This may contain work from a previous Clancy version that squash-merged locally.")),console.log(x(" To preserve this work, push it manually:")),console.log(x(` git push -u origin ${e}`)),console.log(x(" Then re-run /clancy:once to continue.")),!1;try{if(Ou("git",["fetch","origin",t],{encoding:"utf8",stdio:["pipe","pipe","pipe"],timeout:15e3}),Ou("git",["checkout","-b",e,`origin/${t}`],{encoding:"utf8"}),Ye(e))console.log(N(` \u2713 Created epic branch ${e}`));else return console.log(S(`\u26A0 Created ${e} locally but could not push to origin.`)),!1;return!0}catch(o){return console.log(Te(`\u2717 Could not create epic branch: ${o instanceof Error?o.message:String(o)}`)),!1}}async function ic(e,t,r,n,o,i=!1,s,c){if(!Ye(r)){console.log(S(`\u26A0 Could not push ${r} to origin.`)),console.log(x(" The branch is still available locally. Push manually:")),console.log(x(` git push -u origin ${r}`)),i||C(process.cwd(),t.key,t.title,"PUSH_FAILED",void 0,s),Z(n);let y=Ee(Date.now()-o);return console.log(""),console.log(S(`\u26A0 ${t.key} implemented but push failed`)+x(` (${y})`)),!1}console.log(N(` \u2713 Pushed ${r}`));let u;try{let y=ph(process.cwd(),".clancy","verify-attempt.txt"),_=lh(y,"utf8").trim(),z=parseInt(_,10);z>0&&(u=`Verification checks did not fully pass (${z} attempt(s)). Review carefully.`)}catch{}let l=L(e).CLANCY_GIT_PLATFORM,d=ye(l),m=`feat(${t.key}): ${t.title}`,h=dr(e,{key:t.key,title:t.title,description:t.description,provider:e.provider},n,u);if(d.host!=="none"&&d.host!=="unknown"&&d.host!=="azure"){let y=await bt(e,d,r,n,m,h);if(y?.ok)console.log(N(` \u2713 PR created: ${y.url}`)),i||C(process.cwd(),t.key,t.title,"PR_CREATED",y.number,s);else if(y&&!y.ok&&y.alreadyExists)console.log(S(` \u26A0 A PR/MR already exists for ${r}. Branch pushed.`)),i||C(process.cwd(),t.key,t.title,"PUSHED",void 0,s);else if(y&&!y.ok){console.log(S(` \u26A0 PR/MR creation failed: ${y.error}`));let _=hr(d,r,n);console.log(_?x(` Create one manually: ${_}`):x(" Branch pushed \u2014 create a PR/MR manually.")),i||C(process.cwd(),t.key,t.title,"PUSHED",void 0,s)}else{let _=hr(d,r,n);console.log(_?x(` Create a PR: ${_}`):x(" Branch pushed to remote. Create a PR/MR manually.")),i||C(process.cwd(),t.key,t.title,"PUSHED",void 0,s)}}else d.host==="none"?(console.log(S(`\u26A0 No git remote configured. Branch available locally: ${r}`)),i||C(process.cwd(),t.key,t.title,"LOCAL",void 0,s)):(console.log(x(" Branch pushed to remote. Create a PR/MR manually.")),i||C(process.cwd(),t.key,t.title,"PUSHED",void 0,s));if(e.provider!=="github"&&c){let y=e.env.CLANCY_STATUS_REVIEW??e.env.CLANCY_STATUS_DONE;y&&await c.transitionTicket(t,y)}return Z(n),!0}async function ju(e,t,r,n,o,i){console.log(""),console.log(N(`\u{1F389} All children of ${t} are done!`)),console.log(x(` Creating epic PR: ${n} \u2192 ${o}`));let s=U(process.cwd(),"PR_CREATED"),c=U(process.cwd(),"DONE"),a=U(process.cwd(),"REWORK"),u=U(process.cwd(),"PUSHED"),l=[...s,...c,...a,...u].filter(F=>F.parent===t),d=L(e).CLANCY_GIT_PLATFORM,m=ye(d),h=`feat(${t}): ${r}`,y=lu(t,r,l);if(m.host==="none"||m.host==="unknown"||m.host==="azure")return console.log(S("\u26A0 Cannot create epic PR \u2014 no supported git remote detected.")),console.log(x(` Push manually: git push origin ${n}`)),console.log(x(` Then create a PR targeting ${o}`)),!1;let _=await bt(e,m,n,o,h,y);if(_?.ok){if(console.log(N(` \u2713 Epic PR created: ${_.url}`)),C(process.cwd(),t,r,"EPIC_PR_CREATED",_.number),e.provider!=="github"&&i){let F=e.env.CLANCY_STATUS_REVIEW??e.env.CLANCY_STATUS_DONE;F&&await i.transitionTicket({key:t,title:r,description:"",parentInfo:"none",blockers:"None"},F)}return!0}if(_&&!_.ok&&_.alreadyExists)return console.log(S(` \u26A0 An epic PR already exists for ${n}.`)),C(process.cwd(),t,r,"EPIC_PR_CREATED"),!0;console.log(S(`\u26A0 Epic PR creation failed: ${_?.error??"unknown error"}`)),console.log(x(" Create it manually:")),console.log(x(` Branch: ${n} \u2192 ${o}`));let z=hr(m,n,o);return z&&console.log(x(` ${z}`)),!1}async function Lu(e){let t=e.board,r=e.ticket,n=e.ticketBranch,o=e.targetBranch,i=e.baseBranch,s=e.hasParent,c=e.isRework??!1;e.originalBranch=lc();let a=!1;if(s&&!c){let l=await t.fetchChildrenStatus(r.parentInfo,r.linearIssueId);l&&l.total===1&&(a=!0)}e.skipEpicBranch=a;let u=s&&!a?o:i;if(e.effectiveTarget=u,c){if(s&&!a){if(!oc(o,i))return e.originalBranch&&Z(e.originalBranch),!1}else Tr(u,i);vt(n)?Z(n):(Z(u),Z(n,!0))}else if(s&&!a){if(!oc(o,i))return e.originalBranch&&Z(e.originalBranch),!1;Z(o),Z(n,!0)}else Tr(i,i),Z(i),Z(n,!0);try{hc(e.cwd,{pid:process.pid,ticketKey:r.key,ticketTitle:r.title,ticketBranch:n,targetBranch:u,parentKey:r.parentInfo,description:(r.description??"").slice(0,2e3),startedAt:new Date().toISOString()}),e.lockOwner=!0}catch{console.log(x(" (warning: could not write lock file \u2014 crash recovery disabled)"))}return!0}function fh(e){return e.includes("hooks.slack.com")}function dh(e){return JSON.stringify({text:e})}function mh(e){return JSON.stringify({type:"message",attachments:[{contentType:"application/vnd.microsoft.card.adaptive",content:{$schema:"http://adaptivecards.io/schemas/adaptive-card.json",type:"AdaptiveCard",version:"1.4",body:[{type:"TextBlock",text:e,wrap:!0}]}}]})}async function Mu(e,t){let r=fh(e)?dh(t):mh(t);try{let n=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:r});n.ok||console.warn(`\u26A0 Notification failed: HTTP ${n.status}`)}catch{console.warn("\u26A0 Notification failed: could not reach webhook")}}async function Bu(e){let t=e.config,r=e.ticket,n=Ee(Date.now()-e.startTime);console.log(""),console.log(N(`\u{1F3C1} ${r.key} complete`)+x(` (${n})`)),console.log(x(` "Bake 'em away, toys."`));let o=t.env.CLANCY_NOTIFY_WEBHOOK;return o&&await Mu(o,`\u2713 Clancy completed [${r.key}] ${r.title}`),!0}import{appendFileSync as hh,mkdirSync as gh}from"node:fs";import{dirname as _h,join as yh}from"node:path";function Du(e,t,r,n=6600){let o=yh(e,".clancy","costs.log");gh(_h(o),{recursive:!0});let i=new Date(r).getTime(),s=Date.now(),c=Number.isNaN(i)?0:Math.max(0,s-i),a=Math.round(c/6e4),u=Math.round(a*n),d=`${new Date().toISOString()} | ${t} | ${a}min | ~${u} tokens (estimated)
45
+ `)}function lu(e,t,r,n){let o=[];o.push(`## ${e} \u2014 ${t}`),o.push(""),o.push("### Children"),o.push("");for(let i of r){let s=i.prNumber?` (#${i.prNumber})`:"";o.push(`- ${i.key} \u2014 ${i.summary}${s}`)}if(n==="github"){o.push(""),o.push("### Closes"),o.push("");let i=r.map(s=>s.key).filter(s=>s.startsWith("#"));e.startsWith("#")&&i.unshift(e),i.length>0&&o.push(i.map(s=>`Closes ${s}`).join(", "))}return o.push(""),o.push("---"),o.push("*Created by [Clancy](https://github.com/Pushedskydiver/clancy)*"),o.join(`
46
+ `)}import{execFileSync as Gm}from"node:child_process";function pu(e){let t=e.trim().replace(/\.git$/,""),r=t.match(/^git@([^:]+):(.+)$/),n=t.match(/^(?:https?|ssh):\/\/(?:[^@]+@)?([^/:]+)(?::\d+)?\/(.+)$/),o=r?.[1]??n?.[1],i=r?.[2]??n?.[2];if(!(!o||!i))return{hostname:o,path:i}}function qm(e){let t=pu(e);if(!t)return{host:"unknown",url:e};let{hostname:r,path:n}=t;switch(Hm(r)){case"github":{let i=n.split("/");return i.length>=2?{host:"github",owner:i[0],repo:i[1],hostname:r}:{host:"unknown",url:e}}case"gitlab":return{host:"gitlab",projectPath:n,hostname:r};case"bitbucket":{let i=n.match(/^scm\/([^/]+)\/(.+)$/);if(i)return{host:"bitbucket-server",projectKey:i[1],repoSlug:i[2],hostname:r};let s=n.split("/");return s.length>=2?{host:"bitbucket",workspace:s[0],repoSlug:s[1],hostname:r}:{host:"unknown",url:e}}case"azure":return{host:"azure",url:e};default:return{host:"unknown",url:e}}}function Hm(e){let t=e.toLowerCase();return t==="github.com"||t.includes("github")?"github":t==="gitlab.com"||t.includes("gitlab")?"gitlab":t==="bitbucket.org"||t.includes("bitbucket")?"bitbucket":t.includes("dev.azure")||t.includes("visualstudio")?"azure":"unknown"}function ye(e){let t;try{t=Gm("git",["remote","get-url","origin"],{encoding:"utf8"}).trim()}catch{return{host:"none"}}return t?e?Vm(t,e):qm(t):{host:"none"}}function Vm(e,t){let r=pu(e);if(!r)return{host:"unknown",url:e};let{hostname:n,path:o}=r;switch(t.toLowerCase()){case"github":{let i=o.split("/");return i.length>=2?{host:"github",owner:i[0],repo:i[1],hostname:n}:{host:"unknown",url:e}}case"gitlab":return{host:"gitlab",projectPath:o,hostname:n};case"bitbucket":{let i=o.split("/");return i.length>=2?{host:"bitbucket",workspace:i[0],repoSlug:i[1],hostname:n}:{host:"unknown",url:e}}case"bitbucket-server":{let i=o.match(/^scm\/([^/]+)\/(.+)$/);if(i)return{host:"bitbucket-server",projectKey:i[1],repoSlug:i[2],hostname:n};let s=o.split("/");return s.length>=2?{host:"bitbucket-server",projectKey:s[0],repoSlug:s[1],hostname:n}:{host:"unknown",url:e}}default:return{host:"unknown",url:e}}}function _t(e,t){if(t)return t.replace(/\/$/,"");switch(e.host){case"github":return e.hostname==="github.com"?"https://api.github.com":`https://${e.hostname}/api/v3`;case"gitlab":return`https://${e.hostname}/api/v4`;case"bitbucket":return"https://api.bitbucket.org/2.0";case"bitbucket-server":return`https://${e.hostname}/rest/api/1.0`;default:return}}var Wm=E({id:_e(),links:E({html:$(E({href:$(b())}))}),participants:D(E({state:$(b()),role:b()}))}),fu=E({values:D(Wm)}),Xm=E({content:E({raw:b()}),inline:$(E({path:$(b())})),created_on:b(),user:$(E({nickname:$(b())}))}),Qs=E({values:D(Xm)}),Qm=E({id:_e(),links:E({self:$(D(E({href:$(b())})))}),reviewers:D(E({status:b()}))}),du=E({values:D(Qm)}),eh=E({text:b(),anchor:$(E({path:$(b())})),createdDate:_e(),author:$(E({slug:$(b())}))}),th=E({action:b(),comment:$(eh)}),ec=E({values:D(th)});async function Se(e,t,r,n,o){let i=new AbortController,s=setTimeout(()=>i.abort(),3e4);try{let c=await fetch(e,{method:"POST",headers:{...t,"Content-Type":"application/json"},body:JSON.stringify(r),signal:i.signal});if(!c.ok){let l=await c.text().catch(()=>""),d=o?.(c.status,l)??!1;return{ok:!1,error:`HTTP ${c.status}${l?`: ${l.slice(0,200)}`:""}`,alreadyExists:d}}let a=await c.json(),u=n(a);return!u.url&&!u.number?{ok:!1,error:"PR created but response missing URL and number"}:{ok:!0,url:u.url,number:u.number}}catch(c){return{ok:!1,error:c instanceof Error?c.message:String(c)}}finally{clearTimeout(s)}}function Ue(e,t){return`Basic ${Buffer.from(`${e}:${t}`).toString("base64")}`}function te(e){return e.trim().toLowerCase().startsWith("rework:")}function Pe(e){return e.trim().replace(/^rework:\s*/i,"").trim()}async function mu(e,t,r,n,o,i,s,c){return Se(`https://api.bitbucket.org/2.0/repositories/${r}/${n}/pullrequests`,{Authorization:Ue(e,t)},{title:s,description:c,source:{branch:{name:o}},destination:{branch:{name:i}},close_source_branch:!0},a=>{let u=a;return{url:u.links?.html?.href??"",number:u.id??0}},(a,u)=>a===409&&u.includes("already exists"))}async function hu(e,t,r,n,o,i,s,c){return Se(`${t}/projects/${r}/repos/${n}/pull-requests`,{Authorization:`Bearer ${e}`},{title:s,description:c,fromRef:{id:`refs/heads/${o}`,repository:{slug:n,project:{key:r}}},toRef:{id:`refs/heads/${i}`,repository:{slug:n,project:{key:r}}}},a=>{let u=a;return{url:u.links?.self?.[0]?.href??"",number:u.id??0}},(a,u)=>a===409&&(u.includes("already exists")||u.includes("Only one pull request")))}async function gu(e,t,r,n,o,i){try{return(await fetch(`https://api.bitbucket.org/2.0/repositories/${r}/${n}/pullrequests/${o}/comments`,{method:"POST",headers:{Authorization:Ue(e,t),"Content-Type":"application/json"},body:JSON.stringify({content:{raw:i}})})).ok}catch{return!1}}async function _u(e,t,r,n,o,i){try{return(await fetch(`${t}/projects/${r}/repos/${n}/pull-requests/${o}/comments`,{method:"POST",headers:{Authorization:`Bearer ${e}`,"Content-Type":"application/json"},body:JSON.stringify({text:i})})).ok}catch{return!1}}async function yu(e,t,r,n,o,i){try{let s=`https://api.bitbucket.org/2.0/repositories/${r}/${n}/pullrequests?q=source.branch.name="${o}"&state=OPEN`,c=await fetch(s,{headers:{Authorization:Ue(e,t)}});if(!c.ok)return;let a=fu.parse(await c.json());if(a.values.length===0)return;let u=a.values[0],l=u.links.html?.href??"",d=`https://api.bitbucket.org/2.0/repositories/${r}/${n}/pullrequests/${u.id}/comments?pagelen=100`,m=await fetch(d,{headers:{Authorization:Ue(e,t)}});if(!m.ok)return;let h=Qs.parse(await m.json()),y=i?h.values.filter(H=>H.created_on>i):h.values,_=y.some(H=>H.inline!=null),z=y.some(H=>H.inline==null&&te(H.content.raw));return{changesRequested:_||z,prNumber:u.id,prUrl:l}}catch{return}}async function bu(e,t,r,n,o,i){try{let s=`https://api.bitbucket.org/2.0/repositories/${r}/${n}/pullrequests/${o}/comments?pagelen=100`,c=await fetch(s,{headers:{Authorization:Ue(e,t)}});if(!c.ok)return[];let a=Qs.parse(await c.json()),u=i?a.values.filter(d=>d.created_on>i):a.values,l=[];for(let d of u)if(d.inline!=null){let m=d.inline.path?`[${d.inline.path}] `:"";l.push(`${m}${d.content.raw}`)}else te(d.content.raw)&&l.push(Pe(d.content.raw));return l}catch{return[]}}async function $u(e,t,r,n,o,i){try{let s=`${t}/projects/${r}/repos/${n}/pull-requests?state=OPEN&at=refs/heads/${o}`,c=await fetch(s,{headers:{Authorization:`Bearer ${e}`}});if(!c.ok)return;let a=du.parse(await c.json());if(a.values.length===0)return;let u=a.values[0],l=u.links.self?.[0]?.href??"",d=`${t}/projects/${r}/repos/${n}/pull-requests/${u.id}/activities?limit=100`,m=await fetch(d,{headers:{Authorization:`Bearer ${e}`}});if(!m.ok)return;let h=ec.parse(await m.json()),y=i?Date.parse(i):void 0,_=h.values.filter(R=>R.action==="COMMENTED"&&R.comment&&(y==null||R.comment.createdDate>y)),z=_.some(R=>R.comment.anchor!=null),F=_.some(R=>R.comment.anchor==null&&te(R.comment.text));return{changesRequested:z||F,prNumber:u.id,prUrl:l}}catch{return}}async function xu(e,t,r,n,o,i){try{let s=`${t}/projects/${r}/repos/${n}/pull-requests/${o}/activities?limit=100`,c=await fetch(s,{headers:{Authorization:`Bearer ${e}`}});if(!c.ok)return[];let a=ec.parse(await c.json()),u=i?Date.parse(i):void 0,l=[];for(let d of a.values){if(d.action!=="COMMENTED"||!d.comment||u!=null&&d.comment.createdDate<=u)continue;let m=d.comment;if(m.anchor!=null){let h=m.anchor.path?`[${m.anchor.path}] `:"";l.push(`${h}${m.text}`)}else te(m.text)&&l.push(Pe(m.text))}return l}catch{return[]}}var rh=p.object({number:p.number(),title:p.string(),body:p.optional(p.nullable(p.string())),pull_request:p.optional(p.unknown()),milestone:p.optional(p.nullable(p.object({title:p.string()}))),labels:p.optional(p.array(p.object({name:p.optional(p.string())})))}),wu=p.array(rh),nh=p.object({id:p.number(),body:p.optional(p.nullable(p.string())),created_at:p.string(),user:p.optional(p.object({login:p.string()}))}),tc=p.array(nh),oh=p.object({number:p.number(),html_url:p.string(),state:p.string()}),vu=p.array(oh),ih=p.object({state:p.string(),user:p.object({login:p.string()}),submitted_at:p.string()}),ku=p.array(ih),sh=p.object({body:p.optional(p.nullable(p.string())),path:p.optional(p.string()),created_at:p.optional(p.string()),user:p.optional(p.object({login:p.string()}))}),rc=p.array(sh);var q="https://api.github.com";async function mr(e,t,r,n){try{let o=new AbortController,i=setTimeout(()=>o.abort(),1e4),s=await fetch(e,{headers:t,signal:o.signal});if(clearTimeout(i),s.ok)return{ok:!0};let c=r[s.status];return c?{ok:!1,error:c}:{ok:!1,error:`\u2717 HTTP ${s.status}`}}catch{return{ok:!1,error:n}}}function Q(e){return{Authorization:`Bearer ${e}`,Accept:"application/vnd.github+json","X-GitHub-Api-Version":"2022-11-28"}}function be(e){return{Authorization:`Basic ${e}`,Accept:"application/json"}}async function zu(e,t,r,n,o=q,i,s){try{let c=Q(e),a=await fetch(`${o}/repos/${t}/pulls?head=${n}:${r}&state=open`,{headers:c});if(!a.ok)return;let u=vu.parse(await a.json());if(u.length===0)return;let l=u[0],d=i?`&since=${i}`:"",[m,h]=await Promise.all([fetch(`${o}/repos/${t}/pulls/${l.number}/comments?per_page=100${d}`,{headers:c}),fetch(`${o}/repos/${t}/issues/${l.number}/comments?per_page=100${d}`,{headers:c})]);if(!m.ok||!h.ok)return;let y=rc.parse(await m.json()),_=tc.parse(await h.json()),z=s?y.filter(re=>re.user?.login!==s):y,F=s?_.filter(re=>re.user?.login!==s):_,H=z.length>0,R=F.some(re=>re.body&&te(re.body)),O=H||R,J;if(!O)try{let re=await fetch(`${o}/repos/${t}/pulls/${l.number}/reviews?per_page=100`,{headers:c});if(re.ok){let Er=ku.parse(await re.json()),ac=new Map;for(let ie of Er)ie.state==="PENDING"||ie.state==="DISMISSED"||ac.set(ie.user.login,ie.state);let uc=[...ac.entries()].filter(([,ie])=>ie==="CHANGES_REQUESTED");uc.length>0&&(O=!0,J=uc.map(([ie])=>ie))}}catch{}return{changesRequested:O,prNumber:l.number,prUrl:l.html_url,reviewers:J}}catch{return}}async function Su(e,t,r,n=q,o,i){try{let s=Q(e),c=o?`&since=${o}`:"",[a,u]=await Promise.all([fetch(`${n}/repos/${t}/pulls/${r}/comments?per_page=100${c}`,{headers:s}),fetch(`${n}/repos/${t}/issues/${r}/comments?per_page=100${c}`,{headers:s})]);if(!a.ok||!u.ok)return[];let l=rc.parse(await a.json()),d=tc.parse(await u.json()),m=i?l.filter(_=>_.user?.login!==i):l,h=i?d.filter(_=>_.user?.login!==i):d,y=[];for(let _ of m){if(!_.body)continue;let z=_.path?`[${_.path}] `:"";y.push(`${z}${_.body}`)}for(let _ of h)_.body&&te(_.body)&&y.push(Pe(_.body));return y}catch{return[]}}async function Pu(e,t,r,n,o=q){try{return(await fetch(`${o}/repos/${t}/issues/${r}/comments`,{method:"POST",headers:{...Q(e),"Content-Type":"application/json"},body:JSON.stringify({body:n})})).ok}catch{return!1}}async function Eu(e,t,r,n,o=q){try{return(await fetch(`${o}/repos/${t}/pulls/${r}/requested_reviewers`,{method:"POST",headers:{...Q(e),"Content-Type":"application/json"},body:JSON.stringify({reviewers:n})})).ok}catch{return!1}}async function Iu(e,t,r,n,o,i,s=q){return Se(`${s}/repos/${t}/pulls`,Q(e),{title:o,head:r,base:n,body:i},c=>{let a=c;return{url:a.html_url??"",number:a.number??0}},(c,a)=>c===422&&a.includes("already exists"))}var ch=E({iid:_e(),web_url:b(),detailed_merge_status:$(b())}),Tu=D(ch),ah=E({body:b(),resolvable:De(),resolved:$(De()),system:De(),type:$(cr(b())),created_at:$(b()),position:$(E({new_path:$(b())})),author:$(E({username:b()}))}),uh=E({id:$(b()),notes:D(ah)}),nc=D(uh);async function Au(e,t,r,n,o,i,s){let c=encodeURIComponent(r);return Se(`${t}/projects/${c}/merge_requests`,{"PRIVATE-TOKEN":e},{source_branch:n,target_branch:o,title:i,description:s,remove_source_branch:!0},a=>{let u=a;return{url:u.web_url??"",number:u.iid??0}},(a,u)=>a===409&&u.includes("already exists"))}async function Ru(e,t,r,n,o){try{let i=encodeURIComponent(r);return(await fetch(`${t}/projects/${i}/merge_requests/${n}/notes`,{method:"POST",headers:{"PRIVATE-TOKEN":e,"Content-Type":"application/json"},body:JSON.stringify({body:o})})).ok}catch{return!1}}async function Cu(e,t,r,n,o){let i=encodeURIComponent(r),s=0;for(let c of o)try{(await fetch(`${t}/projects/${i}/merge_requests/${n}/discussions/${c}`,{method:"PUT",headers:{"PRIVATE-TOKEN":e,"Content-Type":"application/json"},body:JSON.stringify({resolved:!0})})).ok&&s++}catch{}return s}async function Zu(e,t,r,n,o){try{let i=encodeURIComponent(r),s=`${t}/projects/${i}/merge_requests?source_branch=${n}&state=opened`,c=await fetch(s,{headers:{"PRIVATE-TOKEN":e}});if(!c.ok)return;let a=Tu.parse(await c.json());if(a.length===0)return;let u=a[0],l=`${t}/projects/${i}/merge_requests/${u.iid}/discussions?per_page=100`,d=await fetch(l,{headers:{"PRIVATE-TOKEN":e}});if(!d.ok)return;let m=nc.parse(await d.json()),h=!1;for(let y of m){for(let _ of y.notes)if(!_.system&&!(o&&_.created_at&&_.created_at<=o)){if(_.type==="DiffNote"&&_.resolvable!==!1&&_.resolved!==!0){h=!0;break}if(te(_.body)){h=!0;break}}if(h)break}return{changesRequested:h,prNumber:u.iid,prUrl:u.web_url}}catch{return}}async function Nu(e,t,r,n,o){try{let i=encodeURIComponent(r),s=`${t}/projects/${i}/merge_requests/${n}/discussions?per_page=100`,c=await fetch(s,{headers:{"PRIVATE-TOKEN":e}});if(!c.ok)return{comments:[],discussionIds:[]};let a=nc.parse(await c.json()),u=[],l=[];for(let d of a){let m=!1;for(let h of d.notes)if(!h.system&&!(o&&h.created_at&&h.created_at<=o))if(h.type==="DiffNote"&&h.resolvable!==!1&&h.resolved!==!0){let y=h.position?.new_path?`[${h.position.new_path}] `:"";u.push(`${y}${h.body}`),m=!0}else te(h.body)&&(u.push(Pe(h.body)),m=!0);m&&d.id&&l.push(d.id)}return{comments:u,discussionIds:l}}catch{return{comments:[],discussionIds:[]}}}function yt(e,t){let r=L(e);switch(t.host){case"github":if(r.GITHUB_TOKEN)return{token:r.GITHUB_TOKEN};break;case"gitlab":if(r.GITLAB_TOKEN)return{token:r.GITLAB_TOKEN};break;case"bitbucket":if(r.BITBUCKET_USER&&r.BITBUCKET_TOKEN)return{token:r.BITBUCKET_TOKEN,username:r.BITBUCKET_USER};break;case"bitbucket-server":if(r.BITBUCKET_TOKEN)return{token:r.BITBUCKET_TOKEN};break}}async function bt(e,t,r,n,o,i){let s=yt(e,t);if(!s)return;let c=_t(t,L(e).CLANCY_GIT_API_URL);if(c)switch(t.host){case"github":return Iu(s.token,`${t.owner}/${t.repo}`,r,n,o,i,c);case"gitlab":return Au(s.token,c,t.projectPath,r,n,o,i);case"bitbucket":return mu(s.username,s.token,t.workspace,t.repoSlug,r,n,o,i);case"bitbucket-server":return hu(s.token,c,t.projectKey,t.repoSlug,r,n,o,i);default:return}}function hr(e,t,r){let n=encodeURIComponent(t),o=encodeURIComponent(r);if(e.host==="github")return`https://${e.hostname}/${e.owner}/${e.repo}/compare/${o}...${n}`;if(e.host==="gitlab")return`https://${e.hostname}/${e.projectPath}/-/merge_requests/new?merge_request[source_branch]=${n}&merge_request[target_branch]=${o}`;if(e.host==="bitbucket")return`https://${e.hostname}/${e.workspace}/${e.repoSlug}/pull-requests/new?source=${n}&dest=${o}`;if(e.host==="bitbucket-server")return`https://${e.hostname}/projects/${e.projectKey}/repos/${e.repoSlug}/pull-requests?create&sourceBranch=refs/heads/${n}&targetBranch=refs/heads/${o}`}function oc(e,t){let r=pc(e),n=Ke(e);if(r)return vt(e)?!0:(console.log(S(`\u26A0 Epic branch ${e} exists on remote but could not be fetched.`)),!1);if(n)return console.log(Te(`\u2717 Epic branch ${e} exists locally but not on remote.`)),console.log(S(" This may contain work from a previous Clancy version that squash-merged locally.")),console.log(x(" To preserve this work, push it manually:")),console.log(x(` git push -u origin ${e}`)),console.log(x(" Then re-run /clancy:once to continue.")),!1;try{if(Ou("git",["fetch","origin",t],{encoding:"utf8",stdio:["pipe","pipe","pipe"],timeout:15e3}),Ou("git",["checkout","-b",e,`origin/${t}`],{encoding:"utf8"}),Ye(e))console.log(N(` \u2713 Created epic branch ${e}`));else return console.log(S(`\u26A0 Created ${e} locally but could not push to origin.`)),!1;return!0}catch(o){return console.log(Te(`\u2717 Could not create epic branch: ${o instanceof Error?o.message:String(o)}`)),!1}}async function ic(e,t,r,n,o,i=!1,s,c){if(!Ye(r)){console.log(S(`\u26A0 Could not push ${r} to origin.`)),console.log(x(" The branch is still available locally. Push manually:")),console.log(x(` git push -u origin ${r}`)),i||C(process.cwd(),t.key,t.title,"PUSH_FAILED",void 0,s),Z(n);let y=Ee(Date.now()-o);return console.log(""),console.log(S(`\u26A0 ${t.key} implemented but push failed`)+x(` (${y})`)),!1}console.log(N(` \u2713 Pushed ${r}`));let u;try{let y=ph(process.cwd(),".clancy","verify-attempt.txt"),_=lh(y,"utf8").trim(),z=parseInt(_,10);z>0&&(u=`Verification checks did not fully pass (${z} attempt(s)). Review carefully.`)}catch{}let l=L(e).CLANCY_GIT_PLATFORM,d=ye(l),m=`feat(${t.key}): ${t.title}`,h=dr(e,{key:t.key,title:t.title,description:t.description,provider:e.provider},n,u);if(d.host!=="none"&&d.host!=="unknown"&&d.host!=="azure"){let y=await bt(e,d,r,n,m,h);if(y?.ok)console.log(N(` \u2713 PR created: ${y.url}`)),i||C(process.cwd(),t.key,t.title,"PR_CREATED",y.number,s);else if(y&&!y.ok&&y.alreadyExists)console.log(S(` \u26A0 A PR/MR already exists for ${r}. Branch pushed.`)),i||C(process.cwd(),t.key,t.title,"PUSHED",void 0,s);else if(y&&!y.ok){console.log(S(` \u26A0 PR/MR creation failed: ${y.error}`));let _=hr(d,r,n);console.log(_?x(` Create one manually: ${_}`):x(" Branch pushed \u2014 create a PR/MR manually.")),i||C(process.cwd(),t.key,t.title,"PUSHED",void 0,s)}else{let _=hr(d,r,n);console.log(_?x(` Create a PR: ${_}`):x(" Branch pushed to remote. Create a PR/MR manually.")),i||C(process.cwd(),t.key,t.title,"PUSHED",void 0,s)}}else d.host==="none"?(console.log(S(`\u26A0 No git remote configured. Branch available locally: ${r}`)),i||C(process.cwd(),t.key,t.title,"LOCAL",void 0,s)):(console.log(x(" Branch pushed to remote. Create a PR/MR manually.")),i||C(process.cwd(),t.key,t.title,"PUSHED",void 0,s));if(e.provider!=="github"&&c){let y=e.env.CLANCY_STATUS_REVIEW??e.env.CLANCY_STATUS_DONE;y&&await c.transitionTicket(t,y)}return Z(n),!0}async function ju(e,t,r,n,o,i){console.log(""),console.log(N(`\u{1F389} All children of ${t} are done!`)),console.log(x(` Creating epic PR: ${n} \u2192 ${o}`));let s=U(process.cwd(),"PR_CREATED"),c=U(process.cwd(),"DONE"),a=U(process.cwd(),"REWORK"),u=U(process.cwd(),"PUSHED"),l=[...s,...c,...a,...u].filter(F=>F.parent===t),d=L(e).CLANCY_GIT_PLATFORM,m=ye(d),h=`feat(${t}): ${r}`,y=lu(t,r,l,e.provider);if(m.host==="none"||m.host==="unknown"||m.host==="azure")return console.log(S("\u26A0 Cannot create epic PR \u2014 no supported git remote detected.")),console.log(x(` Push manually: git push origin ${n}`)),console.log(x(` Then create a PR targeting ${o}`)),!1;let _=await bt(e,m,n,o,h,y);if(_?.ok){if(console.log(N(` \u2713 Epic PR created: ${_.url}`)),C(process.cwd(),t,r,"EPIC_PR_CREATED",_.number),e.provider!=="github"&&i){let F=e.env.CLANCY_STATUS_REVIEW??e.env.CLANCY_STATUS_DONE;F&&await i.transitionTicket({key:t,title:r,description:"",parentInfo:"none",blockers:"None"},F)}return!0}if(_&&!_.ok&&_.alreadyExists)return console.log(S(` \u26A0 An epic PR already exists for ${n}.`)),C(process.cwd(),t,r,"EPIC_PR_CREATED"),!0;console.log(S(`\u26A0 Epic PR creation failed: ${_?.error??"unknown error"}`)),console.log(x(" Create it manually:")),console.log(x(` Branch: ${n} \u2192 ${o}`));let z=hr(m,n,o);return z&&console.log(x(` ${z}`)),!1}async function Lu(e){let t=e.board,r=e.ticket,n=e.ticketBranch,o=e.targetBranch,i=e.baseBranch,s=e.hasParent,c=e.isRework??!1;e.originalBranch=lc();let a=!1;if(s&&!c){let l=await t.fetchChildrenStatus(r.parentInfo,r.linearIssueId);l&&l.total===1&&(a=!0)}e.skipEpicBranch=a;let u=s&&!a?o:i;if(e.effectiveTarget=u,c){if(s&&!a){if(!oc(o,i))return e.originalBranch&&Z(e.originalBranch),!1}else Tr(u,i);vt(n)?Z(n):(Z(u),Z(n,!0))}else if(s&&!a){if(!oc(o,i))return e.originalBranch&&Z(e.originalBranch),!1;Z(o),Z(n,!0)}else Tr(i,i),Z(i),Z(n,!0);try{hc(e.cwd,{pid:process.pid,ticketKey:r.key,ticketTitle:r.title,ticketBranch:n,targetBranch:u,parentKey:r.parentInfo,description:(r.description??"").slice(0,2e3),startedAt:new Date().toISOString()}),e.lockOwner=!0}catch{console.log(x(" (warning: could not write lock file \u2014 crash recovery disabled)"))}return!0}function fh(e){return e.includes("hooks.slack.com")}function dh(e){return JSON.stringify({text:e})}function mh(e){return JSON.stringify({type:"message",attachments:[{contentType:"application/vnd.microsoft.card.adaptive",content:{$schema:"http://adaptivecards.io/schemas/adaptive-card.json",type:"AdaptiveCard",version:"1.4",body:[{type:"TextBlock",text:e,wrap:!0}]}}]})}async function Mu(e,t){let r=fh(e)?dh(t):mh(t);try{let n=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:r});n.ok||console.warn(`\u26A0 Notification failed: HTTP ${n.status}`)}catch{console.warn("\u26A0 Notification failed: could not reach webhook")}}async function Bu(e){let t=e.config,r=e.ticket,n=Ee(Date.now()-e.startTime);console.log(""),console.log(N(`\u{1F3C1} ${r.key} complete`)+x(` (${n})`)),console.log(x(` "Bake 'em away, toys."`));let o=t.env.CLANCY_NOTIFY_WEBHOOK;return o&&await Mu(o,`\u2713 Clancy completed [${r.key}] ${r.title}`),!0}import{appendFileSync as hh,mkdirSync as gh}from"node:fs";import{dirname as _h,join as yh}from"node:path";function Du(e,t,r,n=6600){let o=yh(e,".clancy","costs.log");gh(_h(o),{recursive:!0});let i=new Date(r).getTime(),s=Date.now(),c=Number.isNaN(i)?0:Math.max(0,s-i),a=Math.round(c/6e4),u=Math.round(a*n),d=`${new Date().toISOString()} | ${t} | ${a}min | ~${u} tokens (estimated)
47
47
  `;hh(o,d,"utf8")}function Uu(e){let t=e.config,r=e.ticket;try{let n=zt(e.cwd);if(n){let o=Number(t.env.CLANCY_TOKEN_RATE??"6600");Du(e.cwd,r.key,n.startedAt,Number.isFinite(o)&&o>0?o:6600)}}catch{}return!0}var bh=p.object({login:p.string()}),$h=/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/,gr;function _r(e){return $h.test(e)}async function Ju(e,t){return mr(`${q}/repos/${t}`,Q(e),{401:"\u2717 GitHub auth failed \u2014 check GITHUB_TOKEN",403:"\u2717 GitHub permission denied",404:`\u2717 GitHub repo "${t}" not found`},"\u2717 Could not reach GitHub \u2014 check network")}async function Fe(e,t){if(gr)return gr;try{let r=await fetch(`${t??q}/user`,{headers:Q(e)});if(!r.ok){let i=r.status===401||r.status===403?' Fine-grained PATs need "Account permissions \u2192 read" (or classic PATs need read:user scope).':"";return console.warn(`\u26A0 GitHub /user returned HTTP ${r.status} \u2014 falling back to @me.${i}`),"@me"}let n=await r.json(),o=bh.safeParse(n);if(o.success)return gr=o.data.login,gr;console.warn("\u26A0 Unexpected GitHub /user response \u2014 falling back to @me")}catch(r){console.warn(`\u26A0 GitHub /user request failed: ${r instanceof Error?r.message:String(r)} \u2014 falling back to @me`)}return"@me"}async function yr(e,t,r,n,o,i=5){let s,c=new URLSearchParams({state:"open",assignee:n??"@me",per_page:"10"});r&&c.set("labels",r);try{s=await fetch(`${q}/repos/${t}/issues?${c}`,{headers:Q(e)})}catch(d){return console.warn(`\u26A0 GitHub API request failed: ${d instanceof Error?d.message:String(d)}`),[]}if(!s.ok)return console.warn(`\u26A0 GitHub API returned HTTP ${s.status}`),[];let a;try{a=await s.json()}catch{return console.warn("\u26A0 GitHub API returned invalid JSON"),[]}let u=wu.safeParse(a);if(!u.success)return console.warn(`\u26A0 Unexpected GitHub response shape: ${u.error.message}`),[];let l=u.data.filter(d=>!d.pull_request);return o&&(l=l.filter(d=>!d.labels?.some(m=>m.name==="clancy:hitl"))),l.slice(0,i).map(d=>({key:`#${d.number}`,title:d.title,description:d.body??"",provider:"github",milestone:d.milestone?.title}))}async function br(e,t,r,n){if(!_r(t))return!1;let o=/Blocked by #(\d+)/gi,i=new Set,s;for(;(s=o.exec(n))!==null;){let c=parseInt(s[1],10);!Number.isNaN(c)&&c!==r&&i.add(c)}if(!i.size)return!1;try{for(let c of i){let a=await fetch(`${q}/repos/${t}/issues/${c}`,{headers:Q(e)});if(!a.ok)continue;if((await a.json()).state!=="closed")return!0}return!1}catch{return!1}}async function Ku(e,t,r){if(_r(t))try{let n=await Fu(e,t,`Epic: #${r}`);return n&&n.total>0?n:await Fu(e,t,`Parent: #${r}`)}catch{return}}async function Fu(e,t,r){let n=Q(e),o=`"${r}" repo:${t} is:issue`,i=new URLSearchParams({q:o,per_page:"1"}),s=await fetch(`${q}/search/issues?${i}`,{headers:n});if(!s.ok)return;let a=(await s.json()).total_count??0;if(a===0)return{total:0,incomplete:0};let u=`"${r}" repo:${t} is:issue is:open`,l=new URLSearchParams({q:u,per_page:"1"}),d=await fetch(`${q}/search/issues?${l}`,{headers:n});if(!d.ok)return{total:a,incomplete:a};let h=(await d.json()).total_count??0;return{total:a,incomplete:h}}function $r(e,t){return e==="github"?`feature/issue-${t.replace("#","")}`:`feature/${t.toLowerCase()}`}function xr(e,t,r){return r?e==="github"?`milestone/${r.toLowerCase().replace(/\s+/g,"-").replace(/[^a-z0-9-]/g,"")}`:`epic/${r.toLowerCase()}`:t}async function Yu(e){let t=U(process.cwd(),"PR_CREATED"),r=U(process.cwd(),"REWORK"),n=U(process.cwd(),"PUSHED"),o=U(process.cwd(),"PUSH_FAILED"),i=[...t,...r,...n,...o];if(i.length===0)return;let s=L(e).CLANCY_GIT_PLATFORM,c=ye(s);if(c.host==="none"||c.host==="unknown"||c.host==="azure")return;let a=yt(e,c);if(!a)return;let u=_t(c,L(e).CLANCY_GIT_API_URL);if(!u)return;let l;if(c.host==="github")try{l=await Fe(a.token,u)}catch{}let d=i.slice(0,5);for(let m of d){let h=$r(e.provider,m.key),y;if(m.timestamp){let z=new Date(m.timestamp.replace(" ","T")+"Z");y=Number.isNaN(z.getTime())?void 0:z.toISOString()}let _;switch(c.host){case"github":_=await zu(a.token,`${c.owner}/${c.repo}`,h,c.owner,u,y,l);break;case"gitlab":_=await Zu(a.token,u,c.projectPath,h,y);break;case"bitbucket":_=await yu(a.username,a.token,c.workspace,c.repoSlug,h,y);break;case"bitbucket-server":_=await $u(a.token,u,c.projectKey,c.repoSlug,h,y);break}if(_?.changesRequested){let z=[],F;switch(c.host){case"github":z=await Su(a.token,`${c.owner}/${c.repo}`,_.prNumber,u,y,l);break;case"gitlab":{let R=await Nu(a.token,u,c.projectPath,_.prNumber,y);z=R.comments,F=R.discussionIds;break}case"bitbucket":z=await bu(a.username,a.token,c.workspace,c.repoSlug,_.prNumber,y);break;case"bitbucket-server":z=await xu(a.token,u,c.projectKey,c.repoSlug,_.prNumber,y);break}return{ticket:{key:m.key,title:m.summary,description:m.summary,parentInfo:m.parent??"none",blockers:"None"},feedback:z,prNumber:_.prNumber,discussionIds:F,reviewers:_.reviewers??[]}}}}function xh(e){if(e.length===0)return"[clancy] Rework pushed addressing reviewer feedback.";let t=e.length,r=e.slice(0,3).map(o=>`- ${o.slice(0,80)}`).join(`
48
48
  `),n=e.length>3?`
49
49
  - ...`:"";return`[clancy] Rework pushed addressing ${t} feedback item${t!==1?"s":""}.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chief-clancy",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Autonomous, board-driven development for Claude Code — scaffolds docs, integrates Kanban boards, runs tickets in a loop.",
5
5
  "keywords": [
6
6
  "claude",
@@ -2,6 +2,9 @@
2
2
 
3
3
  Promote an approved Clancy plan to the ticket description. Accepts an optional ticket key argument (e.g. `/clancy:approve-plan PROJ-123`). When no key is provided, auto-selects the oldest planned-but-unapproved ticket.
4
4
 
5
+ Optional flags:
6
+ - **Skip confirmation:** `--afk` — auto-confirm without prompting (for automation)
7
+
5
8
  @.claude/clancy/workflows/approve-plan.md
6
9
 
7
10
  Follow the approve-plan workflow above. Fetch the plan comment, confirm with the user, append it to the ticket description, and transition the ticket to the implementation queue.
@@ -6,6 +6,7 @@ Accepts optional arguments:
6
6
  - **Batch mode:** `/clancy:plan 3` — plan up to 3 tickets from the queue
7
7
  - **Specific ticket:** `/clancy:plan PROJ-123`, `/clancy:plan #42`, `/clancy:plan ENG-42` — plan a single ticket by key
8
8
  - **Fresh start:** `--fresh` — discard any existing plan and start over
9
+ - **Skip confirmations:** `--afk` — auto-confirm all prompts (for automation)
9
10
 
10
11
  Examples:
11
12
  - `/clancy:plan` — plan 1 ticket from queue
@@ -154,6 +154,8 @@ If the user picks [2], stop: `Cancelled. No changes made.`
154
154
 
155
155
  **If the user already confirmed via auto-select in Step 2, SKIP this step entirely** (avoid double-confirmation).
156
156
 
157
+ **AFK mode:** If running in AFK mode (`--afk` flag or `CLANCY_MODE=afk`), skip the confirmation prompt and auto-confirm. Display the summary for logging purposes but proceed without waiting for input.
158
+
157
159
  Display a summary and ask for confirmation:
158
160
 
159
161
  ```
@@ -166,7 +168,7 @@ Planned: {date from plan}
166
168
  Promote this plan to the ticket description? [Y/n]
167
169
  ```
168
170
 
169
- If the user declines, stop:
171
+ If the user declines (interactive only), stop:
170
172
  ```
171
173
  Cancelled. No changes made.
172
174
  ```
@@ -66,7 +66,7 @@ Parse the arguments passed to the command:
66
66
 
67
67
  If N > 10: `Maximum batch size is 10. Planning 10 tickets.`
68
68
 
69
- If N >= 5: display a confirmation:
69
+ If N >= 5: display a confirmation (skip in AFK mode — `--afk` flag or `CLANCY_MODE=afk`):
70
70
  ```
71
71
  Planning {N} tickets — each requires codebase exploration. Continue? [Y/n]
72
72
  ```
@@ -92,7 +92,7 @@ RESPONSE=$(curl -s \
92
92
 
93
93
  Validate the response:
94
94
  - If `pull_request` field is present (not null): `#{N} is a PR, not an issue.` Stop.
95
- - If `state` is `closed`: warn `Issue #${N} is closed. Plan anyway? [y/N]`
95
+ - If `state` is `closed`: warn `Issue #${N} is closed. Plan anyway? [y/N]` (in AFK mode: skip this ticket — do not plan closed issues unattended)
96
96
 
97
97
  #### Jira — Fetch specific ticket
98
98
 
@@ -104,7 +104,7 @@ RESPONSE=$(curl -s \
104
104
  ```
105
105
 
106
106
  Validate the response:
107
- - If `fields.status.statusCategory.key` is `done`: warn `Ticket is done. Plan anyway? [y/N]`
107
+ - If `fields.status.statusCategory.key` is `done`: warn `Ticket is done. Plan anyway? [y/N]` (in AFK mode: skip this ticket)
108
108
  - If `fields.issuetype.name` is `Epic`: note `This is an epic.` (continue normally)
109
109
 
110
110
  #### Linear — Fetch specific issue
@@ -124,8 +124,8 @@ query {
124
124
 
125
125
  Validate the response:
126
126
  - If `nodes` is empty: `Issue {KEY} not found on Linear.` Stop.
127
- - If `state.type` is `completed`: warn `Issue is completed. Plan anyway? [y/N]`
128
- - If `state.type` is `canceled`: warn `Issue is canceled. Plan anyway? [y/N]`
127
+ - If `state.type` is `completed`: warn `Issue is completed. Plan anyway? [y/N]` (in AFK mode: skip this ticket)
128
+ - If `state.type` is `canceled`: warn `Issue is canceled. Plan anyway? [y/N]` (in AFK mode: skip this ticket)
129
129
 
130
130
  Then skip to Step 3b with this single ticket.
131
131
 
@@ -8,6 +8,7 @@ Accepts optional arguments:
8
8
  - **By ticket:** `/clancy:approve-brief PROJ-123` — approve brief sourced from this ticket
9
9
  - **Set parent:** `--epic PROJ-50` — override or set the parent epic
10
10
  - **Preview:** `--dry-run` — show what would be created without making API calls
11
+ - **Skip confirmation:** `--afk` — auto-confirm without prompting (for automation)
11
12
 
12
13
  Examples:
13
14
  - `/clancy:approve-brief` — auto-select (if only 1 unapproved brief)
@@ -130,7 +130,9 @@ AFK-ready: {X} | Needs human: {Y}
130
130
  Create {N} tickets? [Y/n]
131
131
  ```
132
132
 
133
- If user declines: `Cancelled. No changes made.` Stop.
133
+ **AFK mode:** If running in AFK mode (`--afk` flag OR `CLANCY_MODE=afk`), skip the confirmation prompt and auto-confirm. Display the ticket list for logging purposes but proceed without waiting for input.
134
+
135
+ If user declines (interactive only): `Cancelled. No changes made.` Stop.
134
136
 
135
137
  ---
136
138
 
@@ -50,7 +50,9 @@ Parse the arguments passed to the command. Arguments can appear in any order.
50
50
 
51
51
  ### Input modes
52
52
 
53
- - **No input (no flags that consume arguments):** Interactive mode — prompt `What's the idea?` and parse the response. If the response looks like a ticket reference (`#42`, `PROJ-123`, `ENG-42`), switch to board ticket mode. Otherwise treat as inline text.
53
+ - **No input (no flags that consume arguments):** Interactive mode — but first check for `--afk`:
54
+ - If running in AFK mode (`--afk` flag OR `CLANCY_MODE=afk`): there is no human to answer. Display: `✗ Cannot run /clancy:brief in AFK mode without a ticket or idea. Use: /clancy:brief --afk #42 (GitHub) or PROJ-123 (Jira) or ENG-42 (Linear), or /clancy:brief --afk "Add dark mode", or /clancy:brief 3 (batch mode — implies --afk).` Stop.
55
+ - Otherwise: prompt `What's the idea?` and parse the response. If the response looks like a ticket reference (`#42`, `PROJ-123`, `ENG-42`), switch to board ticket mode. Otherwise treat as inline text.
54
56
  - **Ticket key** (`PROJ-123`, `#42`, `ENG-42`): Board ticket mode — fetch the ticket from the board API. Validate format per platform:
55
57
  - `#N` — valid for GitHub only. If board is Jira or Linear: `The #N format is for GitHub Issues. Use a ticket key like PROJ-123.` Stop.
56
58
  - `PROJ-123` / `ENG-42` (letters-dash-number) — valid for Jira and Linear. If board is GitHub: `Use #N format for GitHub Issues (e.g. #42).` Stop.
@@ -355,7 +357,7 @@ Scan `.clancy/briefs/` for an existing brief matching this idea:
355
357
 
356
358
  1. **Local brief file** — check for `## Feedback` section appended to `.clancy/briefs/{date}-{slug}.md`
357
359
  2. **Companion file** — check for `.clancy/briefs/{date}-{slug}.feedback.md`
358
- 3. **Board comments** (board-sourced only) — find the most recent `Clancy Strategic Brief` comment on the source ticket. Collect all comments posted AFTER it.
360
+ 3. **Board comments** (board-sourced only) — fetch ALL comments on the source ticket. Scan each comment body for the text `Clancy Strategic Brief` (case-insensitive, match anywhere in the body — it may appear as `# Clancy Strategic Brief`, `## Clancy Strategic Brief`, or just the text). The most recent matching comment is the brief. Collect all comments posted AFTER it as feedback.
359
361
 
360
362
  Board comment feedback filtering per platform:
361
363
  - **GitHub:** comments where `created_at` > brief comment's `created_at` AND `user.login` != resolved username (via `GET /user`)
@@ -490,7 +492,7 @@ Using all gathered context (idea, grill output, research findings), generate the
490
492
  - {Specific risk and mitigation strategy}
491
493
 
492
494
  ---
493
- *Generated by [Clancy](https://github.com/Pushedskydiver/clancy). To request changes: comment on the source ticket or add a ## Feedback section to the brief file, then re-run `/clancy:brief` to revise. To approve: `/clancy:approve-brief`. To start over: `/clancy:brief --fresh`.*
495
+ *Generated by [Clancy](https://github.com/Pushedskydiver/clancy). To answer open questions or request changes: comment on the source ticket or add a ## Feedback section to the brief file, then re-run `/clancy:brief` to revise. To approve: `/clancy:approve-brief`. To start over: `/clancy:brief --fresh`.*
494
496
  ```
495
497
 
496
498
  ### Source field format
@@ -518,11 +520,18 @@ Using all gathered context (idea, grill output, research findings), generate the
518
520
 
519
521
  ### Re-brief revision
520
522
 
521
- If revising from feedback (Step 5), prepend a section before Problem Statement:
523
+ If revising from feedback (Step 5):
524
+
525
+ 1. **Cross-reference feedback against Open Questions.** For each Open Question in the existing brief, check if the feedback contains an answer (exact or paraphrased — match by intent, not syntax). Resolved questions move to `## Discovery` with `(Source: human)` tag. Unresolved questions stay in `## Open Questions`.
526
+
527
+ 2. **Apply all other feedback** — changes to scope, goals, decomposition, user stories, etc.
528
+
529
+ 3. **Prepend a section** before Problem Statement:
522
530
 
523
531
  ```markdown
524
532
  ### Changes From Previous Brief
525
- {What feedback was addressed and how the brief changed}
533
+ {What feedback was addressed and how the brief changed.
534
+ List resolved open questions explicitly.}
526
535
  ```
527
536
 
528
537
  ---
@@ -657,27 +666,36 @@ Print the full brief to stdout, followed by the sign-off:
657
666
  ### Next steps (board-sourced)
658
667
 
659
668
  ```
660
- Next steps:
661
- To request changes:
662
- • Comment on {KEY} on your board, then re-run /clancy:brief {KEY} to revise
663
- • Or add a ## Feedback section to the brief file and re-run
664
- To approve: /clancy:approve-brief {KEY}
665
- To start over: /clancy:brief --fresh {KEY}
669
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
670
+ Next Steps
671
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
672
+
673
+ Answer open questions or request changes:
674
+ Comment on {KEY} on your board
675
+ • Or add a ## Feedback section to the brief file
676
+ Then re-run: /clancy:brief {KEY}
677
+
678
+ Approve: /clancy:approve-brief {KEY}
679
+ Start over: /clancy:brief --fresh {KEY}
666
680
  ```
667
681
 
668
682
  ### Next steps (inline text / from file)
669
683
 
670
684
  ```
671
- Next steps:
672
- To request changes:
685
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
686
+ Next Steps
687
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
688
+
689
+ Answer open questions or request changes:
673
690
  • Add a ## Feedback section to:
674
- .clancy/briefs/{date}-{slug}.md
691
+ .clancy/briefs/{date}-{slug}.md
675
692
  • Or create a companion file:
676
- .clancy/briefs/{date}-{slug}.feedback.md
677
- Then re-run /clancy:brief to revise.
678
- To approve: /clancy:approve-brief {slug}
679
- To attach to a parent: /clancy:approve-brief {slug} --epic {KEY}
680
- To start over: /clancy:brief --fresh
693
+ .clancy/briefs/{date}-{slug}.feedback.md
694
+ Then re-run: /clancy:brief
695
+
696
+ Approve: /clancy:approve-brief {slug}
697
+ With parent: /clancy:approve-brief {slug} --epic {KEY}
698
+ Start over: /clancy:brief --fresh
681
699
  ```
682
700
 
683
701
  ---
@@ -726,7 +744,7 @@ Briefs saved to .clancy/briefs/. Run /clancy:approve-brief to create tickets.
726
744
  - The `--list` flag is an inventory display only — no brief generated, no API calls beyond the local filesystem.
727
745
  - Batch mode (`/clancy:brief 3`) implies AI-grill — each ticket is briefed autonomously.
728
746
  - All board API calls are best-effort — if a comment fails to post, print the brief and warn. The local file is the source of truth.
729
- - The `## Clancy Strategic Brief` marker in comments is used by both `/clancy:brief` (to detect existing briefs) and `/clancy:approve-brief` (to find the brief).
747
+ - The `Clancy Strategic Brief` text in comments is the marker used by both `/clancy:brief` (to detect existing briefs and feedback) and `/clancy:approve-brief` (to find the brief). Search case-insensitively and match regardless of heading level (`#`, `##`, or plain text).
730
748
  - Jira uses ADF for comments (with `codeBlock` fallback). GitHub and Linear accept Markdown directly.
731
749
  - Linear personal API keys do NOT use `Bearer` prefix.
732
750
  - Jira uses the new `POST /rest/api/3/search/jql` endpoint (old GET `/search` removed Aug 2025).