From 6439a41c3fc7b762ad1358e012b5c1256e6deafa Mon Sep 17 00:00:00 2001 From: CYJiang Date: Thu, 6 Aug 2026 13:59:02 +0800 Subject: [PATCH] feat: add Trae hook support --- CHANGELOG.md | 6 + assets/manifest.json | 2 +- .../comet/scripts/comet-hook-router.mjs | 2 +- domains/bundle/bundle-platform.ts | 13 ++ domains/bundle/types.ts | 3 +- domains/comet-entry/hook-adapter.ts | 2 + domains/skill/platform-inspect.ts | 31 +++- domains/skill/platform-install.ts | 59 +++++- domains/skill/uninstall.ts | 84 +++++++-- package-lock.json | 4 +- package.json | 2 +- platform/install/platforms.ts | 14 +- test/app/cli-help.test.ts | 2 +- test/app/doctor.test.ts | 3 + test/domains/bundle/bundle-distribute.test.ts | 33 ++++ test/domains/bundle/bundle-platform.test.ts | 18 ++ test/domains/comet-entry/hook-adapter.test.ts | 8 + .../comet-entry/hook-router-runtime.test.ts | 48 ++++- test/domains/skill/platform-inspect.test.ts | 7 + test/domains/skill/skills.test.ts | 169 ++++++++++++++++++ test/domains/skill/uninstall.test.ts | 48 +++++ test/platform/detect.test.ts | 4 + test/repository/release-metadata.test.ts | 2 +- 23 files changed, 534 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a103290d4..cd655bedf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to @rpamis/comet will be documented in this file. +## What's Changed [0.4.0-beta.18] - 2026-08-07 + +### Added + +- **Trae Hook support**: `comet init`, `comet update`, `comet doctor`, and `comet uninstall` now support managed Hook Router entries for Trae and Trae CN, using Trae's official project and global `hooks.json` locations while preserving user-owned Hook configuration. + ## What's Changed [0.4.0-beta.17] - 2026-08-05 ### Fixed diff --git a/assets/manifest.json b/assets/manifest.json index 3ee2c2608..2cfed5859 100644 --- a/assets/manifest.json +++ b/assets/manifest.json @@ -1,5 +1,5 @@ { - "version": "0.4.0-beta.17", + "version": "0.4.0-beta.18", "skills": [ "comet/SKILL.md", "comet-classic/SKILL.md", diff --git a/assets/skills/comet/scripts/comet-hook-router.mjs b/assets/skills/comet/scripts/comet-hook-router.mjs index a40e2dbd8..c1eb6858a 100644 --- a/assets/skills/comet/scripts/comet-hook-router.mjs +++ b/assets/skills/comet/scripts/comet-hook-router.mjs @@ -139,7 +139,7 @@ ${i.comment}`:i.comment}this.doc.range[2]=i.offset;break}default:this.errors.pus `)+1;for(;i!==0;)this.onNewLine(this.offset+i),i=this.source.indexOf(` `,i)+1}yield*this.pop();break;default:yield*this.pop(),yield*this.step()}}*blockMap(e){let i=e.items[e.items.length-1];switch(this.type){case"newline":if(this.onKeyLine=!1,i.value){let r="end"in i.value?i.value.end:void 0;(Array.isArray(r)?r[r.length-1]:void 0)?.type==="comment"?r?.push(this.sourceToken):e.items.push({start:[this.sourceToken]})}else i.sep?i.sep.push(this.sourceToken):i.start.push(this.sourceToken);return;case"space":case"comment":if(i.value)e.items.push({start:[this.sourceToken]});else if(i.sep)i.sep.push(this.sourceToken);else{if(this.atIndentedComment(i.start,e.indent)){let n=e.items[e.items.length-2]?.value?.end;if(Array.isArray(n)){Tr(n,i.start),n.push(this.sourceToken),e.items.pop();return}}i.start.push(this.sourceToken)}return}if(this.indent>=e.indent){let r=!this.onKeyLine&&this.indent===e.indent,n=r&&(i.sep||i.explicitKey)&&this.type!=="seq-item-ind",o=[];if(n&&i.sep&&!i.value){let a=[];for(let s=0;se.indent&&(a.length=0);break;default:a.length=0}}a.length>=2&&(o=i.sep.splice(a[1]))}switch(this.type){case"anchor":case"tag":n||i.value?(o.push(this.sourceToken),e.items.push({start:o}),this.onKeyLine=!0):i.sep?i.sep.push(this.sourceToken):i.start.push(this.sourceToken);return;case"explicit-key-ind":!i.sep&&!i.explicitKey?(i.start.push(this.sourceToken),i.explicitKey=!0):n||i.value?(o.push(this.sourceToken),e.items.push({start:o,explicitKey:!0})):this.stack.push({type:"block-map",offset:this.offset,indent:this.indent,items:[{start:[this.sourceToken],explicitKey:!0}]}),this.onKeyLine=!0;return;case"map-value-ind":if(i.explicitKey)if(i.sep)if(i.value)e.items.push({start:[],key:null,sep:[this.sourceToken]});else if(_e(i.sep,"map-value-ind"))this.stack.push({type:"block-map",offset:this.offset,indent:this.indent,items:[{start:o,key:null,sep:[this.sourceToken]}]});else if(Rl(i.key)&&!_e(i.sep,"newline")){let a=ht(i.start),s=i.key,c=i.sep;c.push(this.sourceToken),delete i.key,delete i.sep,this.stack.push({type:"block-map",offset:this.offset,indent:this.indent,items:[{start:a,key:s,sep:c}]})}else o.length>0?i.sep=i.sep.concat(o,this.sourceToken):i.sep.push(this.sourceToken);else if(_e(i.start,"newline"))Object.assign(i,{key:null,sep:[this.sourceToken]});else{let a=ht(i.start);this.stack.push({type:"block-map",offset:this.offset,indent:this.indent,items:[{start:a,key:null,sep:[this.sourceToken]}]})}else i.sep?i.value||n?e.items.push({start:o,key:null,sep:[this.sourceToken]}):_e(i.sep,"map-value-ind")?this.stack.push({type:"block-map",offset:this.offset,indent:this.indent,items:[{start:[],key:null,sep:[this.sourceToken]}]}):i.sep.push(this.sourceToken):Object.assign(i,{key:null,sep:[this.sourceToken]});this.onKeyLine=!0;return;case"alias":case"scalar":case"single-quoted-scalar":case"double-quoted-scalar":{let a=this.flowScalar(this.type);n||i.value?(e.items.push({start:o,key:a,sep:[]}),this.onKeyLine=!0):i.sep?this.stack.push(a):(Object.assign(i,{key:a,sep:[]}),this.onKeyLine=!0);return}default:{let a=this.startBlockValue(e);if(a){if(a.type==="block-seq"){if(!i.explicitKey&&i.sep&&!_e(i.sep,"newline")){yield*this.pop({type:"error",offset:this.offset,message:"Unexpected block-seq-ind on same line with key",source:this.source});return}}else r&&e.items.push({start:o});this.stack.push(a);return}}}}yield*this.pop(),yield*this.step()}*blockSequence(e){let i=e.items[e.items.length-1];switch(this.type){case"newline":if(i.value){let r="end"in i.value?i.value.end:void 0;(Array.isArray(r)?r[r.length-1]:void 0)?.type==="comment"?r?.push(this.sourceToken):e.items.push({start:[this.sourceToken]})}else i.start.push(this.sourceToken);return;case"space":case"comment":if(i.value)e.items.push({start:[this.sourceToken]});else{if(this.atIndentedComment(i.start,e.indent)){let n=e.items[e.items.length-2]?.value?.end;if(Array.isArray(n)){Tr(n,i.start),n.push(this.sourceToken),e.items.pop();return}}i.start.push(this.sourceToken)}return;case"anchor":case"tag":if(i.value||this.indent<=e.indent)break;i.start.push(this.sourceToken);return;case"seq-item-ind":if(this.indent!==e.indent)break;i.value||_e(i.start,"seq-item-ind")?e.items.push({start:[this.sourceToken]}):i.start.push(this.sourceToken);return}if(this.indent>e.indent){let r=this.startBlockValue(e);if(r){this.stack.push(r);return}}yield*this.pop(),yield*this.step()}*flowCollection(e){let i=e.items[e.items.length-1];if(this.type==="flow-error-end"){let r;do yield*this.pop(),r=this.peek(1);while(r?.type==="flow-collection")}else if(e.end.length===0){switch(this.type){case"comma":case"explicit-key-ind":!i||i.sep?e.items.push({start:[this.sourceToken]}):i.start.push(this.sourceToken);return;case"map-value-ind":!i||i.value?e.items.push({start:[],key:null,sep:[this.sourceToken]}):i.sep?i.sep.push(this.sourceToken):Object.assign(i,{key:null,sep:[this.sourceToken]});return;case"space":case"comment":case"newline":case"anchor":case"tag":!i||i.value?e.items.push({start:[this.sourceToken]}):i.sep?i.sep.push(this.sourceToken):i.start.push(this.sourceToken);return;case"alias":case"scalar":case"single-quoted-scalar":case"double-quoted-scalar":{let n=this.flowScalar(this.type);!i||i.value?e.items.push({start:[],key:n,sep:[]}):i.sep?this.stack.push(n):Object.assign(i,{key:n,sep:[]});return}case"flow-map-end":case"flow-seq-end":e.end.push(this.sourceToken);return}let r=this.startBlockValue(e);r?this.stack.push(r):(yield*this.pop(),yield*this.step())}else{let r=this.peek(2);if(r.type==="block-map"&&(this.type==="map-value-ind"&&r.indent===e.indent||this.type==="newline"&&!r.items[r.items.length-1].sep))yield*this.pop(),yield*this.step();else if(this.type==="map-value-ind"&&r.type!=="flow-collection"){let n=Ar(r),o=ht(n);xl(e);let a=e.end.splice(1,e.end.length);a.push(this.sourceToken);let s={type:"block-map",offset:e.offset,indent:e.indent,items:[{start:o,key:e,sep:a}]};this.onKeyLine=!0,this.stack[this.stack.length-1]=s}else yield*this.lineEnd(e)}}flowScalar(e){if(this.onNewLine){let i=this.source.indexOf(` `)+1;for(;i!==0;)this.onNewLine(this.offset+i),i=this.source.indexOf(` -`,i)+1}return{type:e,offset:this.offset,indent:this.indent,source:this.source}}startBlockValue(e){switch(this.type){case"alias":case"scalar":case"single-quoted-scalar":case"double-quoted-scalar":return this.flowScalar(this.type);case"block-scalar-header":return{type:"block-scalar",offset:this.offset,indent:this.indent,props:[this.sourceToken],source:""};case"flow-map-start":case"flow-seq-start":return{type:"flow-collection",offset:this.offset,indent:this.indent,start:this.sourceToken,items:[],end:[]};case"seq-item-ind":return{type:"block-seq",offset:this.offset,indent:this.indent,items:[{start:[this.sourceToken]}]};case"explicit-key-ind":{this.onKeyLine=!0;let i=Ar(e),r=ht(i);return r.push(this.sourceToken),{type:"block-map",offset:this.offset,indent:this.indent,items:[{start:r,explicitKey:!0}]}}case"map-value-ind":{this.onKeyLine=!0;let i=Ar(e),r=ht(i);return{type:"block-map",offset:this.offset,indent:this.indent,items:[{start:r,key:null,sep:[this.sourceToken]}]}}}return null}atIndentedComment(e,i){return this.type!=="comment"||this.indent<=i?!1:e.every(r=>r.type==="newline"||r.type==="space")}*documentEnd(e){this.type!=="doc-mode"&&(e.end?e.end.push(this.sourceToken):e.end=[this.sourceToken],this.type==="newline"&&(yield*this.pop()))}*lineEnd(e){switch(this.type){case"comma":case"doc-start":case"doc-end":case"flow-seq-end":case"flow-map-end":case"map-value-ind":yield*this.pop(),yield*this.step();break;case"newline":this.onKeyLine=!1;default:e.end?e.end.push(this.sourceToken):e.end=[this.sourceToken],this.type==="newline"&&(yield*this.pop())}}};_l.Parser=ta});var jl=v(ri=>{"use strict";var Al=qo(),rg=Jt(),ii=Xt(),ng=Mn(),og=C(),ag=ea(),Tl=ia();function Ol(t){let e=t.prettyErrors!==!1;return{lineCounter:t.lineCounter||e&&new ag.LineCounter||null,prettyErrors:e}}function sg(t,e={}){let{lineCounter:i,prettyErrors:r}=Ol(e),n=new Tl.Parser(i?.addNewLine),o=new Al.Composer(e),a=Array.from(o.compose(n.parse(t)));if(r&&i)for(let s of a)s.errors.forEach(ii.prettifyError(t,i)),s.warnings.forEach(ii.prettifyError(t,i));return a.length>0?a:Object.assign([],{empty:!0},o.streamInfo())}function Il(t,e={}){let{lineCounter:i,prettyErrors:r}=Ol(e),n=new Tl.Parser(i?.addNewLine),o=new Al.Composer(e),a=null;for(let s of o.compose(n.parse(t),!0,t.length))if(!a)a=s;else if(a.options.logLevel!=="silent"){a.errors.push(new ii.YAMLParseError(s.range.slice(0,2),"MULTIPLE_DOCS","Source contains multiple documents; please use YAML.parseAllDocuments()"));break}return r&&i&&(a.errors.forEach(ii.prettifyError(t,i)),a.warnings.forEach(ii.prettifyError(t,i))),a}function cg(t,e,i){let r;typeof e=="function"?r=e:i===void 0&&e&&typeof e=="object"&&(i=e);let n=Il(t,i);if(!n)return null;if(n.warnings.forEach(o=>ng.warn(n.options.logLevel,o)),n.errors.length>0){if(n.options.logLevel!=="silent")throw n.errors[0];n.errors=[]}return n.toJS(Object.assign({reviver:r},i))}function lg(t,e,i){let r=null;if(typeof e=="function"||Array.isArray(e)?r=e:i===void 0&&e&&(i=e),typeof i=="string"&&(i=i.length),typeof i=="number"){let n=Math.round(i);i=n<1?void 0:n>8?{indent:8}:{indent:n}}if(t===void 0){let{keepUndefined:n}=i??e??{};if(!n)return}return og.isDocument(t)&&!r?t.toString(i):new rg.Document(t,r,i).toString(i)}ri.parse=cg;ri.parseAllDocuments=sg;ri.parseDocument=Il;ri.stringify=lg});var mt=v(x=>{"use strict";var ug=qo(),fg=Jt(),dg=ko(),ra=Xt(),hg=Ot(),Ae=C(),mg=Ce(),pg=O(),gg=xe(),wg=Re(),vg=Rr(),yg=Qo(),bg=ea(),Ng=ia(),Or=jl(),Ll=Rt();x.Composer=ug.Composer;x.Document=fg.Document;x.Schema=dg.Schema;x.YAMLError=ra.YAMLError;x.YAMLParseError=ra.YAMLParseError;x.YAMLWarning=ra.YAMLWarning;x.Alias=hg.Alias;x.isAlias=Ae.isAlias;x.isCollection=Ae.isCollection;x.isDocument=Ae.isDocument;x.isMap=Ae.isMap;x.isNode=Ae.isNode;x.isPair=Ae.isPair;x.isScalar=Ae.isScalar;x.isSeq=Ae.isSeq;x.Pair=mg.Pair;x.Scalar=pg.Scalar;x.YAMLMap=gg.YAMLMap;x.YAMLSeq=wg.YAMLSeq;x.CST=vg;x.Lexer=yg.Lexer;x.LineCounter=bg.LineCounter;x.Parser=Ng.Parser;x.parse=Or.parse;x.parseAllDocuments=Or.parseAllDocuments;x.parseDocument=Or.parseDocument;x.stringify=Or.stringify;x.visit=Ll.visit;x.visitAsync=Ll.visitAsync});import Ir from"path";function Sg(t,e){if(typeof t!="string")throw new Error(`${e} must be a string`);let i=t.trim();if(i.length===0||Ir.posix.isAbsolute(i)||Ir.win32.isAbsolute(i)||/^(?:~|[\\/])/u.test(i))throw new Error(`${e} must be a project-relative path`);if(i===".")return[];let r=i.replaceAll("\\","/").split("/");if(r.some(n=>n===".."))throw new Error(`${e} must stay inside the project root`);if(r.some(n=>n===""||n==="."))throw new Error(`${e} must not contain empty or dot path segments`);return r}function pt(t){let e=Sg(t,"native.artifact_root");return e.length===0?".":e.join("/")}function Bl(t,e="docs"){let i=t??e;if(i!=="legacy"&&i!=="docs")throw new Error("classic.artifact_layout must be legacy or docs");return i}function ai(t,e,i=!1){if(typeof t!="string")throw new Error(`${e} must be a string`);let r=t.trim().replaceAll("\\","/");if(r.length===0||Ir.posix.isAbsolute(r)||Ir.win32.isAbsolute(r)||/^(?:~|[\\/])/u.test(r))throw new Error(`${e} must be relative to its declared path base`);let n=r.split("/");if(n.some(o=>o===".."))throw new Error(`${e} must stay inside its declared path base`);if(n.some(o=>o===""||o==="."))throw new Error(`${e} must not contain empty or dot path segments`);if(!i&&/[*?]/u.test(r))throw new Error(`${e} cannot contain wildcards`);return n.join("/")}function ni(t,e){if(!t||typeof t!="object"||Array.isArray(t))throw new Error(`${e} must be a mapping`);return t}function Ml(t,e,i){let r=t??e;if(r!=="en"&&r!=="zh-CN")throw new Error(`${i} must be en or zh-CN`);return r}function $r(t,e){if(typeof t!="string"||t.length===0||t.includes("\\")||t.includes("\0")||t.startsWith("/")||t.split("/").includes(".."))throw new Error(`${e} contains an unsafe pattern`);if(t.length>jr)throw new Error(`${e} exceeds ${jr} characters`);let i=0;for(let r=0;rLr)throw new Error(`${e} contains more than ${Lr} wildcard tokens`);return t}function $l(t,e,i){if(t===void 0)return[...i];if(!Array.isArray(t))throw new Error(`${e} contains an unsafe pattern`);return[...new Set(t.map(r=>$r(r,e)))].sort((r,n)=>r.localeCompare(n,"en"))}function na(t,e,i){let r=t??e;if(!Number.isSafeInteger(r)||r<1)throw new Error(`${i} must be a positive integer`);return r}function Eg(t){if(t===void 0)return{...le,include:[...le.include],exclude:[...le.exclude]};let e=ni(t,"native.snapshot");return{include:$l(e.include,"native.snapshot.include",le.include),exclude:$l(e.exclude,"native.snapshot.exclude",le.exclude),max_files:na(e.max_files,le.max_files,"native.snapshot.max_files"),max_total_bytes:na(e.max_total_bytes,le.max_total_bytes,"native.snapshot.max_total_bytes"),max_duration_ms:na(e.max_duration_ms,le.max_duration_ms,"native.snapshot.max_duration_ms")}}function Cg(t){if(t===void 0)return;let e=ni(t,"native.pending_root_move"),i=e.id,r=e.from_artifact_root,n=e.to_artifact_root,o=e.stage;if(typeof i!="string"||!/^[a-f0-9-]{8,}$/u.test(i))throw new Error("native.pending_root_move.id is invalid");if(typeof r!="string"||typeof n!="string")throw new Error("native.pending_root_move roots must be strings");if(o!=="copying"&&o!=="ready"&&o!=="switched")throw new Error("native.pending_root_move.stage is invalid");let a;if(e.cleanup!==void 0){let s=ni(e.cleanup,"native.pending_root_move.cleanup"),c=s.kind,l=s.state,f=s.manifest_hash;if(c!=="forward-source"&&c!=="restart-staging"&&c!=="rollback-destination"&&c!=="rollback-staging")throw new Error("native.pending_root_move.cleanup.kind is invalid");if(l!=="prepared"&&l!=="quarantined"&&l!=="deleting")throw new Error("native.pending_root_move.cleanup.state is invalid");if(typeof f!="string"||!/^[a-f0-9]{64}$/u.test(f))throw new Error("native.pending_root_move.cleanup.manifest_hash is invalid");a={kind:c,state:l,manifestHash:f}}return{id:i,fromArtifactRoot:pt(r),toArtifactRoot:pt(n),stage:o,...a?{cleanup:a}:{}}}function Pg(t,e={}){let i=ni(t,"native"),r=i.artifact_root??(e.allowMissingArtifactRoot?"docs":void 0);if(typeof r!="string")throw new Error("native.artifact_root must be a string");let n=i.clarification_mode??"sequential";if(n!=="sequential"&&n!=="batch")throw new Error("native.clarification_mode must be sequential or batch");let o=i.archive_confirmation??"automatic";if(o!=="automatic"&&o!=="required")throw new Error("native.archive_confirmation must be automatic or required");let a=i.max_verify_failures??Dl;if(!Number.isSafeInteger(a)||a<1)throw new Error("native.max_verify_failures must be a positive integer");let s=Cg(i.pending_root_move);return{artifact_root:pt(r),language:Ml(i.language,"en","native.language"),clarification_mode:n,archive_confirmation:o,max_verify_failures:a,snapshot:Eg(i.snapshot),...s?{pending_root_move:s}:{}}}function xg(t){let e=ni(t,"classic"),i=e.context_compression??"off";if(i!=="off"&&i!=="beta")throw new Error("classic.context_compression must be off or beta");let r=e.review_mode??"standard";if(r!=="off"&&r!=="standard"&&r!=="thorough")throw new Error("classic.review_mode must be off, standard, or thorough");let n=e.auto_transition??!0;if(typeof n!="boolean")throw new Error("classic.auto_transition must be true or false");return{artifact_layout:Bl(e.artifact_layout),language:Ml(e.language,"zh-CN","classic.language"),context_compression:i,review_mode:r,auto_transition:n}}function Rg(t){let e=t??!0;if(typeof e!="boolean")throw new Error("ambient_resume must be true or false");return e}function _g(t,e,i,r,n){let o=t.schema!==void 0;if(!(o||t.default_workflow!==void 0||t.workflows!==void 0||!n.allowPartialProject&&t.native!==void 0)||n.allowPartialProject&&!o)return null;if(t.schema!=="comet.project.v1")throw new Error("Unsupported Comet project schema");if(t.default_workflow!=="native"&&t.default_workflow!=="classic")throw new Error("default_workflow must be native or classic");let s=t.workflows??[t.default_workflow];if(!Array.isArray(s)||s.length===0||s.some(l=>l!=="native"&&l!=="classic"))throw new Error("workflows must contain native and/or classic");let c=[...new Set(s)];if(!c.includes(t.default_workflow))throw new Error("workflows must include default_workflow");if(c.includes("native")&&!e)throw new Error("native must be a mapping");return{schema:"comet.project.v1",default_workflow:t.default_workflow,workflows:c,ambient_resume:r,...e?{native:e}:{},...i?{classic:i}:{}}}function gt(t,e={}){let i=(0,oa.parseDocument)(t,{uniqueKeys:!0});if(i.errors.length>0)throw new Error(`Invalid .comet/config.yaml: ${i.errors[0].message}`);let r=i.toJS();if(r==null)return{value:{},config:null,ambient_resume:!0};if(typeof r!="object"||Array.isArray(r))throw new Error("Invalid .comet/config.yaml: root must be a mapping");let n=r,o=Rg(n.ambient_resume),a=n.native===void 0?void 0:Pg(n.native,{allowMissingArtifactRoot:e.allowMissingNativeFields}),s=n.classic===void 0?void 0:xg(n.classic),c=_g(n,a,s,o,{allowPartialProject:e.allowPartialProject??!1});return{value:n,config:c,ambient_resume:o,...a?{native:a}:{},...s?{classic:s}:{}}}var oa,oi,jr,Lr,Dl,kg,le,ue=B(()=>{"use strict";oa=xt(mt(),1),oi=64*1024,jr=1024,Lr=64,Dl=5,kg=[".agents/skills/**",".amazonq/skills/**",".augment/skills/**",".bob/skills/**",".claude/skills/**",".cline/skills/**",".codebuddy/skills/**",".continue/skills/**",".cospec/skills/**",".crush/skills/**",".cursor/skills/**",".factory/skills/**",".forge/skills/**",".gemini/skills/**",".github/skills/**",".iflow/skills/**",".junie/skills/**",".kilocode/skills/**",".kimi-code/skills/**",".kiro/skills/**",".lingma/skills/**",".mimocode/skills/**",".opencode/skills/**",".pi/skills/**",".qoder/skills/**",".qwen/skills/**",".roo/skills/**",".trae-cn/skills/**",".trae/skills/**",".windsurf/skills/**",".zcode/skills/**"],le={include:["**/*"],exclude:[...kg],max_files:1e4,max_total_bytes:256*1024*1024,max_duration_ms:6e4}});function Te(t){return t!==0&&t!==0n&&t!=="0"}function fe(t,e){return Te(t.dev)&&Te(e.dev)&&Te(t.ino)&&Te(e.ino)}function T(t,e){let i=Te(t.dev)&&Te(e.dev);if(i&&t.dev!==e.dev)return!1;let r=Te(t.ino)&&Te(e.ino);return r&&t.ino!==e.ino?!1:i&&r?!0:t.birthtime===e.birthtime}var fi=B(()=>{"use strict"});import{constants as Dr,promises as We}from"fs";function ua(t){return"birthtimeNs"in t&&typeof t.birthtimeNs=="bigint"?t.birthtimeNs:t.birthtimeMs}function Wl(t){return"ctimeNs"in t&&typeof t.ctimeNs=="bigint"?t.ctimeNs:t.ctimeMs}function di(t){return{dev:t.dev,ino:t.ino,birthtime:ua(t)}}function Br(t,e){let i=di(t),r=di(e);return fe(i,r)?T(i,r):T(i,r)&&ua(t)===ua(e)&&Wl(t)===Wl(e)&&t.size===e.size}async function hi(t,e,i={}){if(!Number.isSafeInteger(e)||e<1)throw new Error("race-safe read byte limit must be a positive integer");let r=i.label??"file",n=i.bigint===!0,o=await We.lstat(t,{bigint:n});if(!o.isFile()||o.isSymbolicLink())throw new ge("not-regular-file",`${r} must be a regular file`);if(BigInt(o.size)>BigInt(e))throw new ge("too-large",`${r} exceeds ${e} bytes`);let a=await We.realpath(t);await i.verify?.("pre-open",{realPath:a,identity:di(o)});let s=process.platform==="win32"?Dr.O_RDONLY:Dr.O_RDONLY|Dr.O_NOFOLLOW|Dr.O_NONBLOCK,c;try{c=await We.open(t,s)}catch(l){throw l.code==="ELOOP"?new ge("not-regular-file",`${r} must be a regular file`,{cause:l}):l}try{let[l,f,u]=await Promise.all([c.stat({bigint:n}),We.lstat(t,{bigint:n}),We.realpath(t)]);if(!l.isFile()||!f.isFile()||f.isSymbolicLink()||u!==a||!Br(o,l)||!Br(o,f))throw new ge("changed",`${r} changed while opening`);await i.verify?.("post-open",{realPath:u,identity:di(l)}),await i.hooks?.afterOpen?.();let d=[],m=0,w=Buffer.allocUnsafe(Math.min(64*1024,e+1));for(;;){let b=e+1-m,{bytesRead:k}=await c.read(w,0,Math.min(w.length,b),null);if(k===0)break;if(m+=k,m>e)throw new ge("too-large",`${r} exceeds ${e} bytes`);d.push(Buffer.from(w.subarray(0,k)))}await i.hooks?.beforeFinalCheck?.();let[h,p,y]=await Promise.all([c.stat({bigint:n}),We.lstat(t,{bigint:n}),We.realpath(t)]);if(!p.isFile()||p.isSymbolicLink()||y!==a||!Br(o,h)||!Br(o,p))throw new ge("changed",`${r} changed while reading`);return await i.verify?.("post-read",{realPath:y,identity:di(h)}),{bytes:Buffer.concat(d,m),stat:h,realPath:y}}finally{await c.close()}}var ge,Mr=B(()=>{"use strict";fi();ge=class extends Error{reason;constructor(e,i,r){super(i,r),this.name="RaceSafeReadError",this.reason=e}}});import{promises as Fr}from"fs";import Oe from"path";function Lg(t){let e=t?.code;return e==="ENOENT"||e==="ENOTDIR"}function fa(t,e){let i=Oe.relative(t,e);return i===""||!Oe.isAbsolute(i)&&i!==".."&&!i.startsWith(`..${Oe.sep}`)}async function ql(t,e){let i=Oe.resolve(t),r=await Fr.lstat(i);if(!r.isDirectory()||r.isSymbolicLink())throw new Error(`${e} project root must be a real directory`);return Fr.realpath(i)}async function Hl(t,e,i,r){let n=t;for(let o=0;o{if(!fa(o,s.realPath))throw new Error(`${r.label} resolves outside the project root`);await Hl(n.projectRoot,o,n.relative.split("/"),{label:r.label,expected:"file"})}})}var we=B(()=>{"use strict";Mr();ue()});import{createHash as $g}from"crypto";function Dg(t){let e=t?.code;return e==="ENOENT"||e==="ENOTDIR"}async function qe(t,e={}){return(await pi(t,e)).document}async function Bg(t){try{return(await te(t,mi,oi,{label:mi})).bytes}catch(e){if(Dg(e))return null;throw e}}function Mg(t){return t?{exists:!0,sha256:$g("sha256").update(t).digest("hex")}:{exists:!1,sha256:null}}async function pi(t,e={}){let i=await Bg(t);return{document:i?gt(i.toString("utf8"),e):null,identity:Mg(i)}}async function da(t){return(await qe(t))?.config??null}var mi,He=B(()=>{"use strict";ue();we();mi=".comet/config.yaml"});import{randomUUID as Fg}from"crypto";import{constants as Wg,promises as I}from"fs";import D from"path";async function ha(t,e){try{return await I.link(t,e),{linked:!0}}catch(i){let r=i.code;if(r!=="ENOTSUP"&&r!=="EOPNOTSUPP")throw i}return await I.copyFile(t,e,Wg.COPYFILE_EXCL),{linked:!1}}function yt(t,e){let i=D.relative(t,e);return i===""||!D.isAbsolute(i)&&i!==".."&&!i.startsWith(`..${D.sep}`)}function qr(t){return{dev:t.dev,ino:t.ino,birthtime:t.birthtimeMs}}function qg(t,e){return T(t,qr(e))}function Wr(t,e){let i=qr(t),r=qr(e);return fe(i,r)?T(i,r):T(i,r)&&t.birthtimeMs===e.birthtimeMs&&t.ctimeMs===e.ctimeMs&&t.size===e.size}async function Hr(t){let e=await I.lstat(t);if(!e.isDirectory()||e.isSymbolicLink())throw new Error(`Contained atomic write parent must be a real directory: ${t}`);return{path:t,realPath:await I.realpath(t),...qr(e)}}async function Ie(t){for(let e of t){let i=await I.lstat(e.path);if(!i.isDirectory()||i.isSymbolicLink()||!qg(e,i)||await I.realpath(e.path)!==e.realPath)throw new Error(`Contained atomic write parent changed before commit: ${e.path}`)}}async function Hg(t,e){let i=D.resolve(t),r=D.resolve(e);if(!yt(i,r))throw new Error(`Contained atomic write parent is outside its managed root: ${e}`);let n=[await Hr(i)],o=D.relative(i,r).split(D.sep).filter(Boolean),a=i;for(let s of o){await Ie(n),a=D.join(a,s);try{await I.mkdir(a)}catch(l){if(l.code!=="EEXIST")throw l}let c=await Hr(a);if(!yt(n[0].realPath,c.realPath))throw new Error(`Contained atomic write parent resolves outside its managed root: ${a}`);n.push(c)}return await Ie(n),n}async function Gg(t,e){let i=D.resolve(t),r=D.resolve(e);if(!yt(i,r))throw new Error(`Contained file parent is outside its managed root: ${e}`);let n=[await Hr(i)],o=D.relative(i,r).split(D.sep).filter(Boolean),a=i;for(let s of o){a=D.join(a,s);try{let c=await Hr(a);if(!yt(n[0].realPath,c.realPath))throw new Error(`Contained file parent resolves outside its managed root: ${a}`);n.push(c)}catch(c){if(c.code==="ENOENT")return null;throw c}}return await Ie(n),n}async function Gl(t){let e;try{e=await I.open(t,"r"),await e.sync()}catch(i){let r=i.code;if(!["EACCES","EBADF","EINVAL","EISDIR","ENOTSUP","EPERM"].includes(r??""))throw i}finally{await e?.close()}}async function zg(t,e,i){let r=D.dirname(t),n=await Hg(i.containedRoot,r),o=D.join(r,`.${D.basename(t)}.${Fg()}.tmp`),a,s;try{await i.beforeTemporaryOpen?.(),a=await I.open(o,"wx"),s=await a.stat();let[c,l]=await Promise.all([I.lstat(o),I.realpath(o)]);if(await Ie(n),!c.isFile()||c.isSymbolicLink()||!Wr(s,c)||!yt(n[0].realPath,l))throw new Error("Contained atomic write temporary file opened outside its managed parent");if(typeof e=="string"?await a.writeFile(e,"utf8"):await a.writeFile(e),await a.sync(),!Wr(s,await a.stat()))throw new Error("Contained atomic write temporary file changed while writing");await a.close(),a=void 0,await i.beforeCommit?.(),await Ie(n);let f=await I.lstat(o);if(!f.isFile()||f.isSymbolicLink()||!Wr(f,s))throw new Error("Contained atomic write temporary file changed before commit");i.exclusive?(await ha(o,t),await I.unlink(o)):await I.rename(o,t),await Gl(r)}catch(c){await a?.close();try{await Ie(n),await I.rm(o,{force:!0})}catch{}throw c}}async function ve(t,e,i){await zg(t,e,i)}async function ma(t,e){let i=D.dirname(t),r=await Gg(e.containedRoot,i);if(!r)return!1;let n,o;try{n=await I.lstat(t),o=await I.realpath(t)}catch(c){if(c.code==="ENOENT")return!1;throw c}if(!n.isFile()||n.isSymbolicLink()||!yt(r[0].realPath,o))throw new Error("Contained file removal target must be a regular file inside its managed root");await e.beforeRemove?.(),await Ie(r);let[a,s]=await Promise.all([I.lstat(t),I.realpath(t)]);if(!a.isFile()||a.isSymbolicLink()||!Wr(n,a)||s!==o)throw new Error("Contained file removal target changed before removal");return await I.unlink(t),await Ie(r),await Gl(i),!0}var Ge=B(()=>{"use strict";fi()});var vk,pa=B(()=>{"use strict";Ge();He();we();vk=16*1024});var zr=B(()=>{"use strict";Ge();ue();He();we();pa()});var Xr=B(()=>{"use strict"});var hu=B(()=>{"use strict"});var Ra=B(()=>{"use strict";Xr()});var mu=B(()=>{"use strict";Xr();Ra()});var Iw,gu=B(()=>{"use strict";Iw=xt(mt(),1);Ge();ue();we()});var wu=B(()=>{"use strict";Xr();Ge();hu();mu();ue();gu();we();He();zr();pa();Ra()});import{TextDecoder as Lw}from"util";function vu(t){return{dev:t.dev,ino:t.ino,birthtime:t.birthtimeMs}}function $w(t,e){try{return new Lw("utf-8",{fatal:!0}).decode(t.bytes)}catch(i){throw new Error(`${e} is not valid UTF-8`,{cause:i})}}async function _a(t,e,i,r,n={}){try{let o=await te(t,e,i,{label:r,hooks:n.hooks});return{result:o,text:$w(o,r)}}catch(o){let a=o.code;if(a==="ENOENT"||a==="ENOTDIR")return null;throw o}}async function yu(t,e,i,r,n={}){return(await _a(t,e,i,r,n))?.text??null}async function bu(t,e,i,r,n,o={}){if(Buffer.byteLength(i,"utf8")>r)throw new Error(`${n} exceeds ${r} bytes`);let a=await _a(t,e,r,n),s=await G(t,e,{label:n,expected:"file"});await ve(s.target,i,{containedRoot:s.projectRoot,beforeCommit:async()=>{await o.beforeCommit?.();let c=await _a(t,e,r,n);if(!(!a&&!c)){if(!a||!c)throw new Error(`${n} changed before commit`);if(a.result.realPath!==c.result.realPath||!T(vu(a.result.stat),vu(c.result.stat))||a.text!==c.text)throw new Error(`${n} changed before commit`)}}})}async function Nu(t,e,i,r={}){let n=await G(t,e,{label:i,expected:"file"});return n.exists?ma(n.target,{containedRoot:n.projectRoot,beforeRemove:r.beforeRemove}):!1}var ku=B(()=>{"use strict";wu();fi()});var xu={};Kf(xu,{RUN_STATE_FILE:()=>Qr,applyRunStateToDocument:()=>Eu,readRunState:()=>Aa,removeRunState:()=>Pu,runStateFromDocument:()=>Si,writeRunState:()=>Ta});import Dw from"path";function ki(t,e){let i=t[e];if(typeof i!="string"||i.length===0)throw new Error(`Invalid Run state: ${e} must be a non-empty string`);return i}function Ni(t,e){let i=ki(t,e);if(Dw.isAbsolute(i)||/^(?:[A-Za-z]:|[\\/]|~)/u.test(i)||i.split(/[\\/]/u).includes(".."))throw new Error(`Invalid Run state: ${e} must stay inside the change directory`);return i}function Bw(t){let e=t.run_retries??"{}",i;try{i=typeof e=="string"?JSON.parse(e):e}catch(r){throw new Error("Invalid Run state: run_retries must be a JSON object",{cause:r})}if(!i||typeof i!="object"||Array.isArray(i))throw new Error("Invalid Run state: run_retries must be a JSON object");for(let r of Object.values(i))if(!Number.isInteger(r)||Number(r)<0)throw new Error("Invalid Run state: retry counts must be non-negative integers");return i}function Si(t){if(!t.run_id)return null;let e=ki(t,"run_id"),i=ki(t,"skill"),r=ki(t,"skill_version"),n=ki(t,"skill_hash"),o=Ni(t,"pending_ref"),a=Ni(t,"trajectory_ref"),s=Ni(t,"context_ref"),c=Ni(t,"artifacts_ref"),l=Ni(t,"checkpoint_ref"),f=Number(t.iteration);if(!Number.isInteger(f)||f<0)throw new Error("Invalid Run state: iteration must be a non-negative integer");if(t.orchestration!=="deterministic"&&t.orchestration!=="adaptive")throw new Error("Invalid Run state: orchestration must be deterministic or adaptive");if(t.run_status!=="running"&&t.run_status!=="waiting"&&t.run_status!=="completed"&&t.run_status!=="failed")throw new Error("Invalid Run state: run_status is invalid");return{runId:e,skill:i,skillVersion:r,skillHash:n,orchestration:t.orchestration,currentStep:Su(t,"current_step"),iteration:f,pending:Su(t,"pending"),pendingRef:o,trajectoryRef:a,contextRef:s,artifactsRef:c,checkpointRef:l,status:t.run_status,retries:Bw(t)}}function Eu(t,e){e?t.run_id=e.runId:delete t.run_id}function Mw(t){return{runId:t.runId,skill:t.skill,skillVersion:t.skillVersion,skillHash:t.skillHash,orchestration:t.orchestration,currentStep:t.currentStep,iteration:t.iteration,pending:t.pending,pendingRef:t.pendingRef,trajectoryRef:t.trajectoryRef,contextRef:t.contextRef,artifactsRef:t.artifactsRef,checkpointRef:t.checkpointRef,status:t.status,retries:t.retries}}function Fw(t){let e={run_id:t.runId,skill:t.skill,skill_version:t.skillVersion,skill_hash:t.skillHash,orchestration:t.orchestration,current_step:t.currentStep,iteration:t.iteration,pending:t.pending,pending_ref:t.pendingRef,trajectory_ref:t.trajectoryRef,context_ref:t.contextRef,artifacts_ref:t.artifactsRef,checkpoint_ref:t.checkpointRef,run_status:t.status,run_retries:JSON.stringify(t.retries)};return Si(e)}async function Aa(t,e={}){let i=await yu(t,Qr,Cu,"Run state",e);if(i===null)return null;let r=JSON.parse(i);return Fw(r)}async function Ta(t,e,i={}){await bu(t,Qr,JSON.stringify(Mw(e),null,2),Cu,"Run state",i)}async function Pu(t,e={}){await Nu(t,Qr,"Run state",e)}var Su,Qr,Cu,Ei=B(()=>{"use strict";ku();Su=(t,e)=>{let i=t[e];return i==null?null:String(i)};Qr=".comet/run-state.json",Cu=256*1024});import Wf from"path";import{promises as Ff}from"fs";ue();import{promises as wt}from"fs";import R from"path";import Ag from"os";var sa=".comet/config.yaml";async function aa(t){try{return await wt.lstat(t),!0}catch(e){if(e.code==="ENOENT")return!1;throw e}}async function Tg(t){try{let e=await wt.readFile(t,"utf8");return/^schema:\s*comet\.project\.v1\s*$/mu.test(e)}catch(e){if(e.code==="ENOENT")return!1;throw e}}function si(t,e){let i=R.relative(t,e);return i===""||!R.isAbsolute(i)&&i!==".."&&!i.startsWith(`..${R.sep}`)}async function ci(t){let e=[],i=t;for(;!await aa(i);){let n=R.dirname(i);if(n===i)break;e.push(R.basename(i)),i=n}let r=await wt.realpath(i);return R.resolve(r,...e.reverse())}async function Fl(t){try{return(await wt.lstat(t)).isSymbolicLink()}catch(e){if(e.code==="ENOENT")return!1;throw e}}async function li(t){let e=R.resolve(t);try{(await wt.stat(e)).isDirectory()||(e=R.dirname(e))}catch(n){if(n.code!=="ENOENT")throw n}let i=e,r=R.resolve(Ag.homedir());for(;;){if(!(e===r&&i!==r)){let a=R.join(e,...sa.split("/")),s=e===i||await Tg(a);if(await aa(a)&&s)return e}if(await aa(R.join(e,".git")))return e;let o=R.dirname(e);if(o===e)return i;e=o}}function ca(t){return pt(t)}async function Og(t,e){let i=ca(e),r=R.resolve(t,...i.split("/")),n=await wt.realpath(t),o=await ci(r);if(!si(n,o))throw new Error("native.artifact_root resolves outside the project root");return r}async function la(t,e){let i=ca(e),r=await Og(t,i),n=R.join(r,"comet");if(await Fl(n))throw new Error("The configured Native comet root must not be a symbolic link");let[o,a]=await Promise.all([ci(r),ci(n)]);if(!si(o,a))throw new Error("The configured Native comet root resolves outside its artifact root");return{projectRoot:R.resolve(t),configFile:R.join(t,...sa.split("/")),artifactRoot:r,artifactRootRef:i,nativeRoot:n,specsDir:R.join(n,"specs"),changesDir:R.join(n,"changes"),archiveDir:R.join(n,"archive"),runtimeDir:R.join(n,"runtime"),locksDir:R.join(n,"runtime","locks"),transactionsDir:R.join(n,"runtime","transactions")}}function vt(t,e){return si(R.resolve(t),R.resolve(e))}async function pe(t,e){let i=R.resolve(t),r=R.resolve(e);if(!si(i,r))throw new Error(`Path is outside the Native root: ${e}`);if(await Fl(i))throw new Error(`Native root must not be a symbolic link: ${t}`);let[n,o]=await Promise.all([ci(i),ci(r)]);if(!si(n,o))throw new Error(`Path resolves outside the Native root: ${e}`);return r}import{promises as Kl}from"fs";import A from"path";import{promises as Ig}from"fs";async function ui(t){try{return await Ig.access(t),!0}catch(e){if(jg(e))return!1;throw e}}function jg(t){let e=t?.code;return e==="ENOENT"||e==="ENOTDIR"}ue();He();zr();we();var ze=class extends Error{code="classic-layout-unavailable";constructor(e="Classic artifact layout is unavailable from .comet/config.yaml"){super(e),this.name="ClassicLayoutUnavailableError"}},Vl=".comet/config.yaml";function Jl(t){let e=t?.code;return e==="ENOENT"||e==="ENOTDIR"}async function Ul(t){await G(t,Vl,{label:Vl,expected:"file"})}function Yl(t,e){let i=A.resolve(t),r=e==="docs"?A.join(i,"docs"):i,n=A.join(r,"openspec"),o=A.join(i,"docs","superpowers");return{projectRoot:i,artifactLayout:e,openSpecBase:r,openSpecRoot:n,changesDir:A.join(n,"changes"),archiveDir:A.join(n,"changes","archive"),specsDir:A.join(n,"specs"),superpowersRoot:o,superpowersSpecsDir:A.join(o,"specs"),superpowersPlansDir:A.join(o,"plans"),superpowersReportsDir:A.join(o,"reports")}}async function Yg(t){await Ul(t);let e=await qe(t,{allowPartialProject:!0,allowMissingNativeFields:!0});if(!e?.config)throw new ze;if(!(e.config.workflows??[e.config.default_workflow]).includes("classic"))throw new ze("Classic artifact layout is unavailable because Classic is not enabled");return e.classic?.artifact_layout??"legacy"}async function Xg(t,e){return Yl(t,e??await Yg(t))}async function Qg(t){try{return await Kl.lstat(t),!0}catch(e){if(Jl(e))return!1;throw e}}async function Zg(t,e){let i=await Xg(t,e),r=i.artifactLayout==="legacy"?"docs":"legacy",n=Yl(t,r).openSpecRoot,[o,a]=await Promise.all([G(i.projectRoot,M(i.projectRoot,i.openSpecRoot),{label:"Configured Classic OpenSpec root",expected:"directory"}),Qg(n)]),s=o.exists;return{paths:i,configuredRootExists:s,alternateRoot:n,alternateRootExists:a,dualRoots:s&&a}}async function ew(t){let e=[t.openSpecRoot,t.changesDir,t.archiveDir,t.specsDir,t.superpowersRoot,t.superpowersSpecsDir,t.superpowersPlansDir,t.superpowersReportsDir];for(let i of e){let r=M(t.projectRoot,i);await G(t.projectRoot,r,{label:`Classic managed physical path ${r}`,expected:"directory"})}}async function ye(t,e){let i=await Zg(t,e);if(await ew(i.paths),!i.configuredRootExists){let r=M(i.paths.projectRoot,i.paths.openSpecRoot),n=M(i.paths.projectRoot,i.alternateRoot);throw new ze(`Configured Classic OpenSpec root is missing: ${r} (alternate ${n} is ${i.alternateRootExists?"present":"missing"})`)}return i.paths}async function Vr(t,e){let i=A.join(A.resolve(t),".comet","classic-root-move.json");if(await ui(i))throw new Error("Classic root move transaction is incomplete; inspect it with comet doctor and recover it explicitly before writing");let r=await ye(t,e);if(!await ui(r.openSpecRoot))throw new Error(`Configured Classic OpenSpec root is missing: ${M(r.projectRoot,r.openSpecRoot)}`);return r}async function Xl(t){let e=A.resolve(t),i=null;try{(await Kl.lstat(e)).isDirectory()||(e=A.dirname(e))}catch(r){if(!Jl(r))throw r}for(;;){A.basename(e)==="openspec"&&(i=A.dirname(e));let r=A.join(e,".comet","config.yaml"),n=!1;if(await ui(r))try{await Ul(e);let a=(await qe(e))?.value;n=!!a&&typeof a=="object"&&!Array.isArray(a)&&(a.schema==="comet.project.v1"||a.default_workflow!==void 0||a.native!==void 0)}catch{}if(n||await ui(A.join(e,".git")))return e;let o=A.dirname(e);if(o===e)return i??A.resolve(t);e=o}}function M(t,e){return A.relative(A.resolve(t),e).replaceAll("\\","/")}import{readFileSync as tw}from"fs";import Ql from"path";var iw=new Set(["applypatch","create","createfile","deletefile","edit","editfile","patch","strreplaceeditor","write","writefile","writefiletool"]),rw=new Set(["glob","grep","listfiles","read","readfile","search","view"]),eu=["file_path","filePath","path","target_file","targetFile"],tu=["file_paths","filePaths","paths","files","targets"],iu=["operations","edits"],nw=["patch","diff","patchText","patch_text","changes"],va=new Set(["claude","codex","windsurf","github-copilot","gemini","amazon-q","qwen","kiro","codebuddy","qoder"]);function ya(t){return!!t&&typeof t=="object"&&!Array.isArray(t)}function Zl(t){return t.toLowerCase().replace(/[^a-z0-9]+/gu,"")}function ow(t){for(let e of["tool_name","toolName","tool","name"]){let i=t[e];if(typeof i=="string"&&i.trim())return i.trim()}return null}function aw(t){for(let e of["cwd","working_directory","workspaceRoot"]){let i=t[e];if(!(typeof i!="string"||!i.trim()||!Ql.isAbsolute(i.trim())))return Ql.resolve(i.trim())}}function sw(t){if(typeof t!="string")return t;let e=t.trim();if(!e.startsWith("{")&&!e.startsWith("["))return t;try{return JSON.parse(e)}catch{return t}}function cw(t){for(let e of["tool_input","toolInput","toolArgs","tool_args","arguments"])if(t[e]!==void 0)return sw(t[e]);return t}function wa(t){let e=[],i=[/^\*\*\* (?:Add|Update|Delete) File:\s+(.+?)\s*$/gmu,/^\+\+\+\s+(?:b\/)?(.+?)\s*$/gmu];for(let r of i)for(let n of t.matchAll(r)){let o=n[1]?.trim();o&&o!=="/dev/null"&&e.push(o)}return e}function Ve(t,e){if(typeof e=="string"){let i=e.trim();i&&t.push(i);return}if(Array.isArray(e)){for(let i of e)Ve(t,i);return}if(ya(e)){for(let i of eu)Ve(t,e[i]);for(let i of tu)Ve(t,e[i]);for(let i of iu)Ve(t,e[i])}}function lw(t,e){let i=[],r=[e,t].filter(ya);for(let n of r){for(let o of eu)Ve(i,n[o]);for(let o of tu)Ve(i,n[o]);for(let o of iu)Ve(i,n[o]);for(let o of nw){let a=n[o];typeof a=="string"&&i.push(...wa(a))}}return typeof e=="string"&&i.push(...wa(e)),[...new Set(i)]}function gi(t,e){if(e?.trim())return{intent:"write",targets:[e.trim()],toolName:null};if(!t.trim())return{intent:"unknown",targets:[],toolName:null};let i;try{i=JSON.parse(t)}catch{let a=wa(t);return a.length>0?{intent:"write",targets:[...new Set(a)],toolName:"apply_patch"}:{intent:"unknown",targets:[],toolName:null}}if(!ya(i))return{intent:"unknown",targets:[],toolName:null};let r=ow(i),n=lw(i,cw(i)),o=aw(i);return r&&iw.has(Zl(r))?{intent:n.length>0?"write":"unknown",targets:n,toolName:r,...o?{cwd:o}:{}}:r&&rw.has(Zl(r))?{intent:"non-write",targets:[],toolName:r,...o?{cwd:o}:{}}:r?{intent:"unknown",targets:n,toolName:r,...o?{cwd:o}:{}}:{intent:n.length>0?"write":"unknown",targets:n,toolName:null,...o?{cwd:o}:{}}}function ba(){let t=process.env.FILE_PATH;if(t?.trim()||process.stdin.isTTY)return gi("",t);try{return gi(tw(0,"utf8"),t)}catch{return gi("",t)}}function ru(t,e){return va.has(t)?t==="github-copilot"?{exitCode:0,stdout:e.allowed?`{} +`,i)+1}return{type:e,offset:this.offset,indent:this.indent,source:this.source}}startBlockValue(e){switch(this.type){case"alias":case"scalar":case"single-quoted-scalar":case"double-quoted-scalar":return this.flowScalar(this.type);case"block-scalar-header":return{type:"block-scalar",offset:this.offset,indent:this.indent,props:[this.sourceToken],source:""};case"flow-map-start":case"flow-seq-start":return{type:"flow-collection",offset:this.offset,indent:this.indent,start:this.sourceToken,items:[],end:[]};case"seq-item-ind":return{type:"block-seq",offset:this.offset,indent:this.indent,items:[{start:[this.sourceToken]}]};case"explicit-key-ind":{this.onKeyLine=!0;let i=Ar(e),r=ht(i);return r.push(this.sourceToken),{type:"block-map",offset:this.offset,indent:this.indent,items:[{start:r,explicitKey:!0}]}}case"map-value-ind":{this.onKeyLine=!0;let i=Ar(e),r=ht(i);return{type:"block-map",offset:this.offset,indent:this.indent,items:[{start:r,key:null,sep:[this.sourceToken]}]}}}return null}atIndentedComment(e,i){return this.type!=="comment"||this.indent<=i?!1:e.every(r=>r.type==="newline"||r.type==="space")}*documentEnd(e){this.type!=="doc-mode"&&(e.end?e.end.push(this.sourceToken):e.end=[this.sourceToken],this.type==="newline"&&(yield*this.pop()))}*lineEnd(e){switch(this.type){case"comma":case"doc-start":case"doc-end":case"flow-seq-end":case"flow-map-end":case"map-value-ind":yield*this.pop(),yield*this.step();break;case"newline":this.onKeyLine=!1;default:e.end?e.end.push(this.sourceToken):e.end=[this.sourceToken],this.type==="newline"&&(yield*this.pop())}}};_l.Parser=ta});var jl=v(ri=>{"use strict";var Al=qo(),rg=Jt(),ii=Xt(),ng=Mn(),og=C(),ag=ea(),Tl=ia();function Ol(t){let e=t.prettyErrors!==!1;return{lineCounter:t.lineCounter||e&&new ag.LineCounter||null,prettyErrors:e}}function sg(t,e={}){let{lineCounter:i,prettyErrors:r}=Ol(e),n=new Tl.Parser(i?.addNewLine),o=new Al.Composer(e),a=Array.from(o.compose(n.parse(t)));if(r&&i)for(let s of a)s.errors.forEach(ii.prettifyError(t,i)),s.warnings.forEach(ii.prettifyError(t,i));return a.length>0?a:Object.assign([],{empty:!0},o.streamInfo())}function Il(t,e={}){let{lineCounter:i,prettyErrors:r}=Ol(e),n=new Tl.Parser(i?.addNewLine),o=new Al.Composer(e),a=null;for(let s of o.compose(n.parse(t),!0,t.length))if(!a)a=s;else if(a.options.logLevel!=="silent"){a.errors.push(new ii.YAMLParseError(s.range.slice(0,2),"MULTIPLE_DOCS","Source contains multiple documents; please use YAML.parseAllDocuments()"));break}return r&&i&&(a.errors.forEach(ii.prettifyError(t,i)),a.warnings.forEach(ii.prettifyError(t,i))),a}function cg(t,e,i){let r;typeof e=="function"?r=e:i===void 0&&e&&typeof e=="object"&&(i=e);let n=Il(t,i);if(!n)return null;if(n.warnings.forEach(o=>ng.warn(n.options.logLevel,o)),n.errors.length>0){if(n.options.logLevel!=="silent")throw n.errors[0];n.errors=[]}return n.toJS(Object.assign({reviver:r},i))}function lg(t,e,i){let r=null;if(typeof e=="function"||Array.isArray(e)?r=e:i===void 0&&e&&(i=e),typeof i=="string"&&(i=i.length),typeof i=="number"){let n=Math.round(i);i=n<1?void 0:n>8?{indent:8}:{indent:n}}if(t===void 0){let{keepUndefined:n}=i??e??{};if(!n)return}return og.isDocument(t)&&!r?t.toString(i):new rg.Document(t,r,i).toString(i)}ri.parse=cg;ri.parseAllDocuments=sg;ri.parseDocument=Il;ri.stringify=lg});var mt=v(x=>{"use strict";var ug=qo(),fg=Jt(),dg=ko(),ra=Xt(),hg=Ot(),Ae=C(),mg=Ce(),pg=O(),gg=xe(),wg=Re(),vg=Rr(),yg=Qo(),bg=ea(),Ng=ia(),Or=jl(),Ll=Rt();x.Composer=ug.Composer;x.Document=fg.Document;x.Schema=dg.Schema;x.YAMLError=ra.YAMLError;x.YAMLParseError=ra.YAMLParseError;x.YAMLWarning=ra.YAMLWarning;x.Alias=hg.Alias;x.isAlias=Ae.isAlias;x.isCollection=Ae.isCollection;x.isDocument=Ae.isDocument;x.isMap=Ae.isMap;x.isNode=Ae.isNode;x.isPair=Ae.isPair;x.isScalar=Ae.isScalar;x.isSeq=Ae.isSeq;x.Pair=mg.Pair;x.Scalar=pg.Scalar;x.YAMLMap=gg.YAMLMap;x.YAMLSeq=wg.YAMLSeq;x.CST=vg;x.Lexer=yg.Lexer;x.LineCounter=bg.LineCounter;x.Parser=Ng.Parser;x.parse=Or.parse;x.parseAllDocuments=Or.parseAllDocuments;x.parseDocument=Or.parseDocument;x.stringify=Or.stringify;x.visit=Ll.visit;x.visitAsync=Ll.visitAsync});import Ir from"path";function Sg(t,e){if(typeof t!="string")throw new Error(`${e} must be a string`);let i=t.trim();if(i.length===0||Ir.posix.isAbsolute(i)||Ir.win32.isAbsolute(i)||/^(?:~|[\\/])/u.test(i))throw new Error(`${e} must be a project-relative path`);if(i===".")return[];let r=i.replaceAll("\\","/").split("/");if(r.some(n=>n===".."))throw new Error(`${e} must stay inside the project root`);if(r.some(n=>n===""||n==="."))throw new Error(`${e} must not contain empty or dot path segments`);return r}function pt(t){let e=Sg(t,"native.artifact_root");return e.length===0?".":e.join("/")}function Bl(t,e="docs"){let i=t??e;if(i!=="legacy"&&i!=="docs")throw new Error("classic.artifact_layout must be legacy or docs");return i}function ai(t,e,i=!1){if(typeof t!="string")throw new Error(`${e} must be a string`);let r=t.trim().replaceAll("\\","/");if(r.length===0||Ir.posix.isAbsolute(r)||Ir.win32.isAbsolute(r)||/^(?:~|[\\/])/u.test(r))throw new Error(`${e} must be relative to its declared path base`);let n=r.split("/");if(n.some(o=>o===".."))throw new Error(`${e} must stay inside its declared path base`);if(n.some(o=>o===""||o==="."))throw new Error(`${e} must not contain empty or dot path segments`);if(!i&&/[*?]/u.test(r))throw new Error(`${e} cannot contain wildcards`);return n.join("/")}function ni(t,e){if(!t||typeof t!="object"||Array.isArray(t))throw new Error(`${e} must be a mapping`);return t}function Ml(t,e,i){let r=t??e;if(r!=="en"&&r!=="zh-CN")throw new Error(`${i} must be en or zh-CN`);return r}function $r(t,e){if(typeof t!="string"||t.length===0||t.includes("\\")||t.includes("\0")||t.startsWith("/")||t.split("/").includes(".."))throw new Error(`${e} contains an unsafe pattern`);if(t.length>jr)throw new Error(`${e} exceeds ${jr} characters`);let i=0;for(let r=0;rLr)throw new Error(`${e} contains more than ${Lr} wildcard tokens`);return t}function $l(t,e,i){if(t===void 0)return[...i];if(!Array.isArray(t))throw new Error(`${e} contains an unsafe pattern`);return[...new Set(t.map(r=>$r(r,e)))].sort((r,n)=>r.localeCompare(n,"en"))}function na(t,e,i){let r=t??e;if(!Number.isSafeInteger(r)||r<1)throw new Error(`${i} must be a positive integer`);return r}function Eg(t){if(t===void 0)return{...le,include:[...le.include],exclude:[...le.exclude]};let e=ni(t,"native.snapshot");return{include:$l(e.include,"native.snapshot.include",le.include),exclude:$l(e.exclude,"native.snapshot.exclude",le.exclude),max_files:na(e.max_files,le.max_files,"native.snapshot.max_files"),max_total_bytes:na(e.max_total_bytes,le.max_total_bytes,"native.snapshot.max_total_bytes"),max_duration_ms:na(e.max_duration_ms,le.max_duration_ms,"native.snapshot.max_duration_ms")}}function Cg(t){if(t===void 0)return;let e=ni(t,"native.pending_root_move"),i=e.id,r=e.from_artifact_root,n=e.to_artifact_root,o=e.stage;if(typeof i!="string"||!/^[a-f0-9-]{8,}$/u.test(i))throw new Error("native.pending_root_move.id is invalid");if(typeof r!="string"||typeof n!="string")throw new Error("native.pending_root_move roots must be strings");if(o!=="copying"&&o!=="ready"&&o!=="switched")throw new Error("native.pending_root_move.stage is invalid");let a;if(e.cleanup!==void 0){let s=ni(e.cleanup,"native.pending_root_move.cleanup"),c=s.kind,l=s.state,f=s.manifest_hash;if(c!=="forward-source"&&c!=="restart-staging"&&c!=="rollback-destination"&&c!=="rollback-staging")throw new Error("native.pending_root_move.cleanup.kind is invalid");if(l!=="prepared"&&l!=="quarantined"&&l!=="deleting")throw new Error("native.pending_root_move.cleanup.state is invalid");if(typeof f!="string"||!/^[a-f0-9]{64}$/u.test(f))throw new Error("native.pending_root_move.cleanup.manifest_hash is invalid");a={kind:c,state:l,manifestHash:f}}return{id:i,fromArtifactRoot:pt(r),toArtifactRoot:pt(n),stage:o,...a?{cleanup:a}:{}}}function Pg(t,e={}){let i=ni(t,"native"),r=i.artifact_root??(e.allowMissingArtifactRoot?"docs":void 0);if(typeof r!="string")throw new Error("native.artifact_root must be a string");let n=i.clarification_mode??"sequential";if(n!=="sequential"&&n!=="batch")throw new Error("native.clarification_mode must be sequential or batch");let o=i.archive_confirmation??"automatic";if(o!=="automatic"&&o!=="required")throw new Error("native.archive_confirmation must be automatic or required");let a=i.max_verify_failures??Dl;if(!Number.isSafeInteger(a)||a<1)throw new Error("native.max_verify_failures must be a positive integer");let s=Cg(i.pending_root_move);return{artifact_root:pt(r),language:Ml(i.language,"en","native.language"),clarification_mode:n,archive_confirmation:o,max_verify_failures:a,snapshot:Eg(i.snapshot),...s?{pending_root_move:s}:{}}}function xg(t){let e=ni(t,"classic"),i=e.context_compression??"off";if(i!=="off"&&i!=="beta")throw new Error("classic.context_compression must be off or beta");let r=e.review_mode??"standard";if(r!=="off"&&r!=="standard"&&r!=="thorough")throw new Error("classic.review_mode must be off, standard, or thorough");let n=e.auto_transition??!0;if(typeof n!="boolean")throw new Error("classic.auto_transition must be true or false");return{artifact_layout:Bl(e.artifact_layout),language:Ml(e.language,"zh-CN","classic.language"),context_compression:i,review_mode:r,auto_transition:n}}function Rg(t){let e=t??!0;if(typeof e!="boolean")throw new Error("ambient_resume must be true or false");return e}function _g(t,e,i,r,n){let o=t.schema!==void 0;if(!(o||t.default_workflow!==void 0||t.workflows!==void 0||!n.allowPartialProject&&t.native!==void 0)||n.allowPartialProject&&!o)return null;if(t.schema!=="comet.project.v1")throw new Error("Unsupported Comet project schema");if(t.default_workflow!=="native"&&t.default_workflow!=="classic")throw new Error("default_workflow must be native or classic");let s=t.workflows??[t.default_workflow];if(!Array.isArray(s)||s.length===0||s.some(l=>l!=="native"&&l!=="classic"))throw new Error("workflows must contain native and/or classic");let c=[...new Set(s)];if(!c.includes(t.default_workflow))throw new Error("workflows must include default_workflow");if(c.includes("native")&&!e)throw new Error("native must be a mapping");return{schema:"comet.project.v1",default_workflow:t.default_workflow,workflows:c,ambient_resume:r,...e?{native:e}:{},...i?{classic:i}:{}}}function gt(t,e={}){let i=(0,oa.parseDocument)(t,{uniqueKeys:!0});if(i.errors.length>0)throw new Error(`Invalid .comet/config.yaml: ${i.errors[0].message}`);let r=i.toJS();if(r==null)return{value:{},config:null,ambient_resume:!0};if(typeof r!="object"||Array.isArray(r))throw new Error("Invalid .comet/config.yaml: root must be a mapping");let n=r,o=Rg(n.ambient_resume),a=n.native===void 0?void 0:Pg(n.native,{allowMissingArtifactRoot:e.allowMissingNativeFields}),s=n.classic===void 0?void 0:xg(n.classic),c=_g(n,a,s,o,{allowPartialProject:e.allowPartialProject??!1});return{value:n,config:c,ambient_resume:o,...a?{native:a}:{},...s?{classic:s}:{}}}var oa,oi,jr,Lr,Dl,kg,le,ue=B(()=>{"use strict";oa=xt(mt(),1),oi=64*1024,jr=1024,Lr=64,Dl=5,kg=[".agents/skills/**",".amazonq/skills/**",".augment/skills/**",".bob/skills/**",".claude/skills/**",".cline/skills/**",".codebuddy/skills/**",".continue/skills/**",".cospec/skills/**",".crush/skills/**",".cursor/skills/**",".factory/skills/**",".forge/skills/**",".gemini/skills/**",".github/skills/**",".iflow/skills/**",".junie/skills/**",".kilocode/skills/**",".kimi-code/skills/**",".kiro/skills/**",".lingma/skills/**",".mimocode/skills/**",".opencode/skills/**",".pi/skills/**",".qoder/skills/**",".qwen/skills/**",".roo/skills/**",".trae-cn/skills/**",".trae/skills/**",".windsurf/skills/**",".zcode/skills/**"],le={include:["**/*"],exclude:[...kg],max_files:1e4,max_total_bytes:256*1024*1024,max_duration_ms:6e4}});function Te(t){return t!==0&&t!==0n&&t!=="0"}function fe(t,e){return Te(t.dev)&&Te(e.dev)&&Te(t.ino)&&Te(e.ino)}function T(t,e){let i=Te(t.dev)&&Te(e.dev);if(i&&t.dev!==e.dev)return!1;let r=Te(t.ino)&&Te(e.ino);return r&&t.ino!==e.ino?!1:i&&r?!0:t.birthtime===e.birthtime}var fi=B(()=>{"use strict"});import{constants as Dr,promises as We}from"fs";function ua(t){return"birthtimeNs"in t&&typeof t.birthtimeNs=="bigint"?t.birthtimeNs:t.birthtimeMs}function Wl(t){return"ctimeNs"in t&&typeof t.ctimeNs=="bigint"?t.ctimeNs:t.ctimeMs}function di(t){return{dev:t.dev,ino:t.ino,birthtime:ua(t)}}function Br(t,e){let i=di(t),r=di(e);return fe(i,r)?T(i,r):T(i,r)&&ua(t)===ua(e)&&Wl(t)===Wl(e)&&t.size===e.size}async function hi(t,e,i={}){if(!Number.isSafeInteger(e)||e<1)throw new Error("race-safe read byte limit must be a positive integer");let r=i.label??"file",n=i.bigint===!0,o=await We.lstat(t,{bigint:n});if(!o.isFile()||o.isSymbolicLink())throw new ge("not-regular-file",`${r} must be a regular file`);if(BigInt(o.size)>BigInt(e))throw new ge("too-large",`${r} exceeds ${e} bytes`);let a=await We.realpath(t);await i.verify?.("pre-open",{realPath:a,identity:di(o)});let s=process.platform==="win32"?Dr.O_RDONLY:Dr.O_RDONLY|Dr.O_NOFOLLOW|Dr.O_NONBLOCK,c;try{c=await We.open(t,s)}catch(l){throw l.code==="ELOOP"?new ge("not-regular-file",`${r} must be a regular file`,{cause:l}):l}try{let[l,f,u]=await Promise.all([c.stat({bigint:n}),We.lstat(t,{bigint:n}),We.realpath(t)]);if(!l.isFile()||!f.isFile()||f.isSymbolicLink()||u!==a||!Br(o,l)||!Br(o,f))throw new ge("changed",`${r} changed while opening`);await i.verify?.("post-open",{realPath:u,identity:di(l)}),await i.hooks?.afterOpen?.();let d=[],m=0,w=Buffer.allocUnsafe(Math.min(64*1024,e+1));for(;;){let b=e+1-m,{bytesRead:k}=await c.read(w,0,Math.min(w.length,b),null);if(k===0)break;if(m+=k,m>e)throw new ge("too-large",`${r} exceeds ${e} bytes`);d.push(Buffer.from(w.subarray(0,k)))}await i.hooks?.beforeFinalCheck?.();let[h,p,y]=await Promise.all([c.stat({bigint:n}),We.lstat(t,{bigint:n}),We.realpath(t)]);if(!p.isFile()||p.isSymbolicLink()||y!==a||!Br(o,h)||!Br(o,p))throw new ge("changed",`${r} changed while reading`);return await i.verify?.("post-read",{realPath:y,identity:di(h)}),{bytes:Buffer.concat(d,m),stat:h,realPath:y}}finally{await c.close()}}var ge,Mr=B(()=>{"use strict";fi();ge=class extends Error{reason;constructor(e,i,r){super(i,r),this.name="RaceSafeReadError",this.reason=e}}});import{promises as Fr}from"fs";import Oe from"path";function Lg(t){let e=t?.code;return e==="ENOENT"||e==="ENOTDIR"}function fa(t,e){let i=Oe.relative(t,e);return i===""||!Oe.isAbsolute(i)&&i!==".."&&!i.startsWith(`..${Oe.sep}`)}async function ql(t,e){let i=Oe.resolve(t),r=await Fr.lstat(i);if(!r.isDirectory()||r.isSymbolicLink())throw new Error(`${e} project root must be a real directory`);return Fr.realpath(i)}async function Hl(t,e,i,r){let n=t;for(let o=0;o{if(!fa(o,s.realPath))throw new Error(`${r.label} resolves outside the project root`);await Hl(n.projectRoot,o,n.relative.split("/"),{label:r.label,expected:"file"})}})}var we=B(()=>{"use strict";Mr();ue()});import{createHash as $g}from"crypto";function Dg(t){let e=t?.code;return e==="ENOENT"||e==="ENOTDIR"}async function qe(t,e={}){return(await pi(t,e)).document}async function Bg(t){try{return(await te(t,mi,oi,{label:mi})).bytes}catch(e){if(Dg(e))return null;throw e}}function Mg(t){return t?{exists:!0,sha256:$g("sha256").update(t).digest("hex")}:{exists:!1,sha256:null}}async function pi(t,e={}){let i=await Bg(t);return{document:i?gt(i.toString("utf8"),e):null,identity:Mg(i)}}async function da(t){return(await qe(t))?.config??null}var mi,He=B(()=>{"use strict";ue();we();mi=".comet/config.yaml"});import{randomUUID as Fg}from"crypto";import{constants as Wg,promises as I}from"fs";import D from"path";async function ha(t,e){try{return await I.link(t,e),{linked:!0}}catch(i){let r=i.code;if(r!=="ENOTSUP"&&r!=="EOPNOTSUPP")throw i}return await I.copyFile(t,e,Wg.COPYFILE_EXCL),{linked:!1}}function yt(t,e){let i=D.relative(t,e);return i===""||!D.isAbsolute(i)&&i!==".."&&!i.startsWith(`..${D.sep}`)}function qr(t){return{dev:t.dev,ino:t.ino,birthtime:t.birthtimeMs}}function qg(t,e){return T(t,qr(e))}function Wr(t,e){let i=qr(t),r=qr(e);return fe(i,r)?T(i,r):T(i,r)&&t.birthtimeMs===e.birthtimeMs&&t.ctimeMs===e.ctimeMs&&t.size===e.size}async function Hr(t){let e=await I.lstat(t);if(!e.isDirectory()||e.isSymbolicLink())throw new Error(`Contained atomic write parent must be a real directory: ${t}`);return{path:t,realPath:await I.realpath(t),...qr(e)}}async function Ie(t){for(let e of t){let i=await I.lstat(e.path);if(!i.isDirectory()||i.isSymbolicLink()||!qg(e,i)||await I.realpath(e.path)!==e.realPath)throw new Error(`Contained atomic write parent changed before commit: ${e.path}`)}}async function Hg(t,e){let i=D.resolve(t),r=D.resolve(e);if(!yt(i,r))throw new Error(`Contained atomic write parent is outside its managed root: ${e}`);let n=[await Hr(i)],o=D.relative(i,r).split(D.sep).filter(Boolean),a=i;for(let s of o){await Ie(n),a=D.join(a,s);try{await I.mkdir(a)}catch(l){if(l.code!=="EEXIST")throw l}let c=await Hr(a);if(!yt(n[0].realPath,c.realPath))throw new Error(`Contained atomic write parent resolves outside its managed root: ${a}`);n.push(c)}return await Ie(n),n}async function Gg(t,e){let i=D.resolve(t),r=D.resolve(e);if(!yt(i,r))throw new Error(`Contained file parent is outside its managed root: ${e}`);let n=[await Hr(i)],o=D.relative(i,r).split(D.sep).filter(Boolean),a=i;for(let s of o){a=D.join(a,s);try{let c=await Hr(a);if(!yt(n[0].realPath,c.realPath))throw new Error(`Contained file parent resolves outside its managed root: ${a}`);n.push(c)}catch(c){if(c.code==="ENOENT")return null;throw c}}return await Ie(n),n}async function Gl(t){let e;try{e=await I.open(t,"r"),await e.sync()}catch(i){let r=i.code;if(!["EACCES","EBADF","EINVAL","EISDIR","ENOTSUP","EPERM"].includes(r??""))throw i}finally{await e?.close()}}async function zg(t,e,i){let r=D.dirname(t),n=await Hg(i.containedRoot,r),o=D.join(r,`.${D.basename(t)}.${Fg()}.tmp`),a,s;try{await i.beforeTemporaryOpen?.(),a=await I.open(o,"wx"),s=await a.stat();let[c,l]=await Promise.all([I.lstat(o),I.realpath(o)]);if(await Ie(n),!c.isFile()||c.isSymbolicLink()||!Wr(s,c)||!yt(n[0].realPath,l))throw new Error("Contained atomic write temporary file opened outside its managed parent");if(typeof e=="string"?await a.writeFile(e,"utf8"):await a.writeFile(e),await a.sync(),!Wr(s,await a.stat()))throw new Error("Contained atomic write temporary file changed while writing");await a.close(),a=void 0,await i.beforeCommit?.(),await Ie(n);let f=await I.lstat(o);if(!f.isFile()||f.isSymbolicLink()||!Wr(f,s))throw new Error("Contained atomic write temporary file changed before commit");i.exclusive?(await ha(o,t),await I.unlink(o)):await I.rename(o,t),await Gl(r)}catch(c){await a?.close();try{await Ie(n),await I.rm(o,{force:!0})}catch{}throw c}}async function ve(t,e,i){await zg(t,e,i)}async function ma(t,e){let i=D.dirname(t),r=await Gg(e.containedRoot,i);if(!r)return!1;let n,o;try{n=await I.lstat(t),o=await I.realpath(t)}catch(c){if(c.code==="ENOENT")return!1;throw c}if(!n.isFile()||n.isSymbolicLink()||!yt(r[0].realPath,o))throw new Error("Contained file removal target must be a regular file inside its managed root");await e.beforeRemove?.(),await Ie(r);let[a,s]=await Promise.all([I.lstat(t),I.realpath(t)]);if(!a.isFile()||a.isSymbolicLink()||!Wr(n,a)||s!==o)throw new Error("Contained file removal target changed before removal");return await I.unlink(t),await Ie(r),await Gl(i),!0}var Ge=B(()=>{"use strict";fi()});var vk,pa=B(()=>{"use strict";Ge();He();we();vk=16*1024});var zr=B(()=>{"use strict";Ge();ue();He();we();pa()});var Xr=B(()=>{"use strict"});var hu=B(()=>{"use strict"});var Ra=B(()=>{"use strict";Xr()});var mu=B(()=>{"use strict";Xr();Ra()});var Iw,gu=B(()=>{"use strict";Iw=xt(mt(),1);Ge();ue();we()});var wu=B(()=>{"use strict";Xr();Ge();hu();mu();ue();gu();we();He();zr();pa();Ra()});import{TextDecoder as Lw}from"util";function vu(t){return{dev:t.dev,ino:t.ino,birthtime:t.birthtimeMs}}function $w(t,e){try{return new Lw("utf-8",{fatal:!0}).decode(t.bytes)}catch(i){throw new Error(`${e} is not valid UTF-8`,{cause:i})}}async function _a(t,e,i,r,n={}){try{let o=await te(t,e,i,{label:r,hooks:n.hooks});return{result:o,text:$w(o,r)}}catch(o){let a=o.code;if(a==="ENOENT"||a==="ENOTDIR")return null;throw o}}async function yu(t,e,i,r,n={}){return(await _a(t,e,i,r,n))?.text??null}async function bu(t,e,i,r,n,o={}){if(Buffer.byteLength(i,"utf8")>r)throw new Error(`${n} exceeds ${r} bytes`);let a=await _a(t,e,r,n),s=await G(t,e,{label:n,expected:"file"});await ve(s.target,i,{containedRoot:s.projectRoot,beforeCommit:async()=>{await o.beforeCommit?.();let c=await _a(t,e,r,n);if(!(!a&&!c)){if(!a||!c)throw new Error(`${n} changed before commit`);if(a.result.realPath!==c.result.realPath||!T(vu(a.result.stat),vu(c.result.stat))||a.text!==c.text)throw new Error(`${n} changed before commit`)}}})}async function Nu(t,e,i,r={}){let n=await G(t,e,{label:i,expected:"file"});return n.exists?ma(n.target,{containedRoot:n.projectRoot,beforeRemove:r.beforeRemove}):!1}var ku=B(()=>{"use strict";wu();fi()});var xu={};Kf(xu,{RUN_STATE_FILE:()=>Qr,applyRunStateToDocument:()=>Eu,readRunState:()=>Aa,removeRunState:()=>Pu,runStateFromDocument:()=>Si,writeRunState:()=>Ta});import Dw from"path";function ki(t,e){let i=t[e];if(typeof i!="string"||i.length===0)throw new Error(`Invalid Run state: ${e} must be a non-empty string`);return i}function Ni(t,e){let i=ki(t,e);if(Dw.isAbsolute(i)||/^(?:[A-Za-z]:|[\\/]|~)/u.test(i)||i.split(/[\\/]/u).includes(".."))throw new Error(`Invalid Run state: ${e} must stay inside the change directory`);return i}function Bw(t){let e=t.run_retries??"{}",i;try{i=typeof e=="string"?JSON.parse(e):e}catch(r){throw new Error("Invalid Run state: run_retries must be a JSON object",{cause:r})}if(!i||typeof i!="object"||Array.isArray(i))throw new Error("Invalid Run state: run_retries must be a JSON object");for(let r of Object.values(i))if(!Number.isInteger(r)||Number(r)<0)throw new Error("Invalid Run state: retry counts must be non-negative integers");return i}function Si(t){if(!t.run_id)return null;let e=ki(t,"run_id"),i=ki(t,"skill"),r=ki(t,"skill_version"),n=ki(t,"skill_hash"),o=Ni(t,"pending_ref"),a=Ni(t,"trajectory_ref"),s=Ni(t,"context_ref"),c=Ni(t,"artifacts_ref"),l=Ni(t,"checkpoint_ref"),f=Number(t.iteration);if(!Number.isInteger(f)||f<0)throw new Error("Invalid Run state: iteration must be a non-negative integer");if(t.orchestration!=="deterministic"&&t.orchestration!=="adaptive")throw new Error("Invalid Run state: orchestration must be deterministic or adaptive");if(t.run_status!=="running"&&t.run_status!=="waiting"&&t.run_status!=="completed"&&t.run_status!=="failed")throw new Error("Invalid Run state: run_status is invalid");return{runId:e,skill:i,skillVersion:r,skillHash:n,orchestration:t.orchestration,currentStep:Su(t,"current_step"),iteration:f,pending:Su(t,"pending"),pendingRef:o,trajectoryRef:a,contextRef:s,artifactsRef:c,checkpointRef:l,status:t.run_status,retries:Bw(t)}}function Eu(t,e){e?t.run_id=e.runId:delete t.run_id}function Mw(t){return{runId:t.runId,skill:t.skill,skillVersion:t.skillVersion,skillHash:t.skillHash,orchestration:t.orchestration,currentStep:t.currentStep,iteration:t.iteration,pending:t.pending,pendingRef:t.pendingRef,trajectoryRef:t.trajectoryRef,contextRef:t.contextRef,artifactsRef:t.artifactsRef,checkpointRef:t.checkpointRef,status:t.status,retries:t.retries}}function Fw(t){let e={run_id:t.runId,skill:t.skill,skill_version:t.skillVersion,skill_hash:t.skillHash,orchestration:t.orchestration,current_step:t.currentStep,iteration:t.iteration,pending:t.pending,pending_ref:t.pendingRef,trajectory_ref:t.trajectoryRef,context_ref:t.contextRef,artifacts_ref:t.artifactsRef,checkpoint_ref:t.checkpointRef,run_status:t.status,run_retries:JSON.stringify(t.retries)};return Si(e)}async function Aa(t,e={}){let i=await yu(t,Qr,Cu,"Run state",e);if(i===null)return null;let r=JSON.parse(i);return Fw(r)}async function Ta(t,e,i={}){await bu(t,Qr,JSON.stringify(Mw(e),null,2),Cu,"Run state",i)}async function Pu(t,e={}){await Nu(t,Qr,"Run state",e)}var Su,Qr,Cu,Ei=B(()=>{"use strict";ku();Su=(t,e)=>{let i=t[e];return i==null?null:String(i)};Qr=".comet/run-state.json",Cu=256*1024});import Wf from"path";import{promises as Ff}from"fs";ue();import{promises as wt}from"fs";import R from"path";import Ag from"os";var sa=".comet/config.yaml";async function aa(t){try{return await wt.lstat(t),!0}catch(e){if(e.code==="ENOENT")return!1;throw e}}async function Tg(t){try{let e=await wt.readFile(t,"utf8");return/^schema:\s*comet\.project\.v1\s*$/mu.test(e)}catch(e){if(e.code==="ENOENT")return!1;throw e}}function si(t,e){let i=R.relative(t,e);return i===""||!R.isAbsolute(i)&&i!==".."&&!i.startsWith(`..${R.sep}`)}async function ci(t){let e=[],i=t;for(;!await aa(i);){let n=R.dirname(i);if(n===i)break;e.push(R.basename(i)),i=n}let r=await wt.realpath(i);return R.resolve(r,...e.reverse())}async function Fl(t){try{return(await wt.lstat(t)).isSymbolicLink()}catch(e){if(e.code==="ENOENT")return!1;throw e}}async function li(t){let e=R.resolve(t);try{(await wt.stat(e)).isDirectory()||(e=R.dirname(e))}catch(n){if(n.code!=="ENOENT")throw n}let i=e,r=R.resolve(Ag.homedir());for(;;){if(!(e===r&&i!==r)){let a=R.join(e,...sa.split("/")),s=e===i||await Tg(a);if(await aa(a)&&s)return e}if(await aa(R.join(e,".git")))return e;let o=R.dirname(e);if(o===e)return i;e=o}}function ca(t){return pt(t)}async function Og(t,e){let i=ca(e),r=R.resolve(t,...i.split("/")),n=await wt.realpath(t),o=await ci(r);if(!si(n,o))throw new Error("native.artifact_root resolves outside the project root");return r}async function la(t,e){let i=ca(e),r=await Og(t,i),n=R.join(r,"comet");if(await Fl(n))throw new Error("The configured Native comet root must not be a symbolic link");let[o,a]=await Promise.all([ci(r),ci(n)]);if(!si(o,a))throw new Error("The configured Native comet root resolves outside its artifact root");return{projectRoot:R.resolve(t),configFile:R.join(t,...sa.split("/")),artifactRoot:r,artifactRootRef:i,nativeRoot:n,specsDir:R.join(n,"specs"),changesDir:R.join(n,"changes"),archiveDir:R.join(n,"archive"),runtimeDir:R.join(n,"runtime"),locksDir:R.join(n,"runtime","locks"),transactionsDir:R.join(n,"runtime","transactions")}}function vt(t,e){return si(R.resolve(t),R.resolve(e))}async function pe(t,e){let i=R.resolve(t),r=R.resolve(e);if(!si(i,r))throw new Error(`Path is outside the Native root: ${e}`);if(await Fl(i))throw new Error(`Native root must not be a symbolic link: ${t}`);let[n,o]=await Promise.all([ci(i),ci(r)]);if(!si(n,o))throw new Error(`Path resolves outside the Native root: ${e}`);return r}import{promises as Kl}from"fs";import A from"path";import{promises as Ig}from"fs";async function ui(t){try{return await Ig.access(t),!0}catch(e){if(jg(e))return!1;throw e}}function jg(t){let e=t?.code;return e==="ENOENT"||e==="ENOTDIR"}ue();He();zr();we();var ze=class extends Error{code="classic-layout-unavailable";constructor(e="Classic artifact layout is unavailable from .comet/config.yaml"){super(e),this.name="ClassicLayoutUnavailableError"}},Vl=".comet/config.yaml";function Jl(t){let e=t?.code;return e==="ENOENT"||e==="ENOTDIR"}async function Ul(t){await G(t,Vl,{label:Vl,expected:"file"})}function Yl(t,e){let i=A.resolve(t),r=e==="docs"?A.join(i,"docs"):i,n=A.join(r,"openspec"),o=A.join(i,"docs","superpowers");return{projectRoot:i,artifactLayout:e,openSpecBase:r,openSpecRoot:n,changesDir:A.join(n,"changes"),archiveDir:A.join(n,"changes","archive"),specsDir:A.join(n,"specs"),superpowersRoot:o,superpowersSpecsDir:A.join(o,"specs"),superpowersPlansDir:A.join(o,"plans"),superpowersReportsDir:A.join(o,"reports")}}async function Yg(t){await Ul(t);let e=await qe(t,{allowPartialProject:!0,allowMissingNativeFields:!0});if(!e?.config)throw new ze;if(!(e.config.workflows??[e.config.default_workflow]).includes("classic"))throw new ze("Classic artifact layout is unavailable because Classic is not enabled");return e.classic?.artifact_layout??"legacy"}async function Xg(t,e){return Yl(t,e??await Yg(t))}async function Qg(t){try{return await Kl.lstat(t),!0}catch(e){if(Jl(e))return!1;throw e}}async function Zg(t,e){let i=await Xg(t,e),r=i.artifactLayout==="legacy"?"docs":"legacy",n=Yl(t,r).openSpecRoot,[o,a]=await Promise.all([G(i.projectRoot,M(i.projectRoot,i.openSpecRoot),{label:"Configured Classic OpenSpec root",expected:"directory"}),Qg(n)]),s=o.exists;return{paths:i,configuredRootExists:s,alternateRoot:n,alternateRootExists:a,dualRoots:s&&a}}async function ew(t){let e=[t.openSpecRoot,t.changesDir,t.archiveDir,t.specsDir,t.superpowersRoot,t.superpowersSpecsDir,t.superpowersPlansDir,t.superpowersReportsDir];for(let i of e){let r=M(t.projectRoot,i);await G(t.projectRoot,r,{label:`Classic managed physical path ${r}`,expected:"directory"})}}async function ye(t,e){let i=await Zg(t,e);if(await ew(i.paths),!i.configuredRootExists){let r=M(i.paths.projectRoot,i.paths.openSpecRoot),n=M(i.paths.projectRoot,i.alternateRoot);throw new ze(`Configured Classic OpenSpec root is missing: ${r} (alternate ${n} is ${i.alternateRootExists?"present":"missing"})`)}return i.paths}async function Vr(t,e){let i=A.join(A.resolve(t),".comet","classic-root-move.json");if(await ui(i))throw new Error("Classic root move transaction is incomplete; inspect it with comet doctor and recover it explicitly before writing");let r=await ye(t,e);if(!await ui(r.openSpecRoot))throw new Error(`Configured Classic OpenSpec root is missing: ${M(r.projectRoot,r.openSpecRoot)}`);return r}async function Xl(t){let e=A.resolve(t),i=null;try{(await Kl.lstat(e)).isDirectory()||(e=A.dirname(e))}catch(r){if(!Jl(r))throw r}for(;;){A.basename(e)==="openspec"&&(i=A.dirname(e));let r=A.join(e,".comet","config.yaml"),n=!1;if(await ui(r))try{await Ul(e);let a=(await qe(e))?.value;n=!!a&&typeof a=="object"&&!Array.isArray(a)&&(a.schema==="comet.project.v1"||a.default_workflow!==void 0||a.native!==void 0)}catch{}if(n||await ui(A.join(e,".git")))return e;let o=A.dirname(e);if(o===e)return i??A.resolve(t);e=o}}function M(t,e){return A.relative(A.resolve(t),e).replaceAll("\\","/")}import{readFileSync as tw}from"fs";import Ql from"path";var iw=new Set(["applypatch","create","createfile","deletefile","edit","editfile","patch","strreplaceeditor","write","writefile","writefiletool"]),rw=new Set(["glob","grep","listfiles","read","readfile","search","view"]),eu=["file_path","filePath","path","target_file","targetFile"],tu=["file_paths","filePaths","paths","files","targets"],iu=["operations","edits"],nw=["patch","diff","patchText","patch_text","changes"],va=new Set(["claude","codex","windsurf","github-copilot","gemini","amazon-q","qwen","kiro","codebuddy","qoder","trae","trae-cn"]);function ya(t){return!!t&&typeof t=="object"&&!Array.isArray(t)}function Zl(t){return t.toLowerCase().replace(/[^a-z0-9]+/gu,"")}function ow(t){for(let e of["tool_name","toolName","tool","name"]){let i=t[e];if(typeof i=="string"&&i.trim())return i.trim()}return null}function aw(t){for(let e of["cwd","working_directory","workspaceRoot"]){let i=t[e];if(!(typeof i!="string"||!i.trim()||!Ql.isAbsolute(i.trim())))return Ql.resolve(i.trim())}}function sw(t){if(typeof t!="string")return t;let e=t.trim();if(!e.startsWith("{")&&!e.startsWith("["))return t;try{return JSON.parse(e)}catch{return t}}function cw(t){for(let e of["tool_input","toolInput","toolArgs","tool_args","arguments"])if(t[e]!==void 0)return sw(t[e]);return t}function wa(t){let e=[],i=[/^\*\*\* (?:Add|Update|Delete) File:\s+(.+?)\s*$/gmu,/^\+\+\+\s+(?:b\/)?(.+?)\s*$/gmu];for(let r of i)for(let n of t.matchAll(r)){let o=n[1]?.trim();o&&o!=="/dev/null"&&e.push(o)}return e}function Ve(t,e){if(typeof e=="string"){let i=e.trim();i&&t.push(i);return}if(Array.isArray(e)){for(let i of e)Ve(t,i);return}if(ya(e)){for(let i of eu)Ve(t,e[i]);for(let i of tu)Ve(t,e[i]);for(let i of iu)Ve(t,e[i])}}function lw(t,e){let i=[],r=[e,t].filter(ya);for(let n of r){for(let o of eu)Ve(i,n[o]);for(let o of tu)Ve(i,n[o]);for(let o of iu)Ve(i,n[o]);for(let o of nw){let a=n[o];typeof a=="string"&&i.push(...wa(a))}}return typeof e=="string"&&i.push(...wa(e)),[...new Set(i)]}function gi(t,e){if(e?.trim())return{intent:"write",targets:[e.trim()],toolName:null};if(!t.trim())return{intent:"unknown",targets:[],toolName:null};let i;try{i=JSON.parse(t)}catch{let a=wa(t);return a.length>0?{intent:"write",targets:[...new Set(a)],toolName:"apply_patch"}:{intent:"unknown",targets:[],toolName:null}}if(!ya(i))return{intent:"unknown",targets:[],toolName:null};let r=ow(i),n=lw(i,cw(i)),o=aw(i);return r&&iw.has(Zl(r))?{intent:n.length>0?"write":"unknown",targets:n,toolName:r,...o?{cwd:o}:{}}:r&&rw.has(Zl(r))?{intent:"non-write",targets:[],toolName:r,...o?{cwd:o}:{}}:r?{intent:"unknown",targets:n,toolName:r,...o?{cwd:o}:{}}:{intent:n.length>0?"write":"unknown",targets:n,toolName:null,...o?{cwd:o}:{}}}function ba(){let t=process.env.FILE_PATH;if(t?.trim()||process.stdin.isTTY)return gi("",t);try{return gi(tw(0,"utf8"),t)}catch{return gi("",t)}}function ru(t,e){return va.has(t)?t==="github-copilot"?{exitCode:0,stdout:e.allowed?`{} `:`${JSON.stringify({permissionDecision:"deny",permissionDecisionReason:e.reason})} `,stderr:""}:e.allowed?{exitCode:0,stdout:"",stderr:""}:{exitCode:2,stdout:"",stderr:`${e.reason} `}:{exitCode:64,stdout:"",stderr:`Unsupported Comet Hook platform: ${t} diff --git a/domains/bundle/bundle-platform.ts b/domains/bundle/bundle-platform.ts index 28e3305ff..335e322ff 100644 --- a/domains/bundle/bundle-platform.ts +++ b/domains/bundle/bundle-platform.ts @@ -118,6 +118,7 @@ function hookDestination(target: BundlePlatformTarget, hookId: string): string | case 'gemini': return path.join(platformRoot, 'settings.json'); case 'windsurf': + case 'trae': return path.join(platformRoot, 'hooks.json'); case 'copilot': return path.join(platformRoot, 'hooks', `${hookId}.json`); @@ -291,6 +292,18 @@ async function applyHookInstallFile(file: PlatformInstallFile): Promise { await writeFile(file.destination, JSON.stringify(settings, null, 2) + '\n'); return; } + case 'trae': { + hooks.PreToolUse = mergeCommandHookGroup( + asHookGroups(hooks.PreToolUse), + matcher, + { ...commandHook, timeout: 30 }, + operation.command, + ); + settings.version = settings.version ?? 1; + settings.hooks = hooks; + await writeFile(file.destination, JSON.stringify(settings, null, 2) + '\n'); + return; + } case 'gemini': { const geminiMatcher = matcher === 'Write|Edit' ? 'write_file|edit_file' : matcher; hooks.BeforeTool = mergeCommandHookGroup( diff --git a/domains/bundle/types.ts b/domains/bundle/types.ts index f5b7251c4..a09e2fba4 100644 --- a/domains/bundle/types.ts +++ b/domains/bundle/types.ts @@ -334,7 +334,8 @@ export interface PlatformInstallFile { | 'qwen' | 'kiro' | 'qoder' - | 'codebuddy'; + | 'codebuddy' + | 'trae'; event: NormalizedHook['event']; matcher?: string; command: string; diff --git a/domains/comet-entry/hook-adapter.ts b/domains/comet-entry/hook-adapter.ts index 383f8fb62..a040c5985 100644 --- a/domains/comet-entry/hook-adapter.ts +++ b/domains/comet-entry/hook-adapter.ts @@ -43,6 +43,8 @@ export const COMET_HOOK_PLATFORM_IDS = new Set([ 'kiro', 'codebuddy', 'qoder', + 'trae', + 'trae-cn', ]); function isRecord(value: unknown): value is Record { diff --git a/domains/skill/platform-inspect.ts b/domains/skill/platform-inspect.ts index 28e571ada..39ef55335 100644 --- a/domains/skill/platform-inspect.ts +++ b/domains/skill/platform-inspect.ts @@ -117,6 +117,11 @@ function countGroupedHookMatches( groupName: string, expected: ExpectedHookDescriptor, expectedMatcher: (matcher: string) => string = (matcher) => matcher, + isExpectedHandler: ( + handler: Record, + expected: ExpectedHookDescriptor, + ) => boolean = (handler, descriptor) => + handler.type === 'command' && handler.command === descriptor.command, ): number { const hooks = config.hooks; if (!hooks || typeof hooks !== 'object' || Array.isArray(hooks)) return 0; @@ -135,8 +140,7 @@ function countGroupedHookMatches( handler !== null && typeof handler === 'object' && !Array.isArray(handler) && - (handler as Record).type === 'command' && - (handler as Record).command === expected.command, + isExpectedHandler(handler as Record, expected), ).length ); }, 0); @@ -440,6 +444,29 @@ export async function inspectCometHooksForPlatform( countWindsurfHookMatches, ); break; + case 'trae': + inspection = await inspectSingleHookJson( + path.join(platformBase, 'hooks.json'), + expectedHooks, + (config) => collectGroupedCommands(config, 'PreToolUse'), + (config, expected) => + countGroupedHookMatches( + config, + 'PreToolUse', + expected, + undefined, + (handler, descriptor) => { + const timeout = handler.timeout; + return ( + handler.type === 'command' && + handler.command === descriptor.command && + typeof timeout === 'number' && + timeout > 0 + ); + }, + ), + ); + break; case 'copilot': inspection = await inspectSingleHookJson( path.join(platformBase, 'hooks', 'comet-guard.json'), diff --git a/domains/skill/platform-install.ts b/domains/skill/platform-install.ts index 0c7b8c1f6..5d8769283 100644 --- a/domains/skill/platform-install.ts +++ b/domains/skill/platform-install.ts @@ -1006,6 +1006,7 @@ ${content}`; * 'windsurf' — hooks.json with pre_write_code array * 'copilot' — hooks/*.json with preToolUse * 'kiro' — hooks/*.kiro.hook JSON files + * 'trae' — hooks.json with version and PreToolUse grouped command hooks */ async function installCometHooksForPlatform( baseDir: string, @@ -1023,7 +1024,7 @@ async function installCometHooksForPlatform( }; } - if (scope === 'global') { + if (scope === 'global' && platform.hookFormat !== 'trae') { return { status: 'skipped', reason: 'blocking Hooks are project-scoped', @@ -1114,6 +1115,15 @@ async function installCometHooksForPlatform( platformId: platform.id, scope, }); + case 'trae': + return await installTraeHooks( + baseDir, + platformBase, + skillsDir, + hooksConfig, + platform.name, + { platformId: platform.id, scope }, + ); default: return { status: 'failed', reason: `unsupported hook format: ${hookFormat}` }; } @@ -1542,6 +1552,53 @@ async function installWindsurfHooks( return { status: 'installed' }; } +/** + * Trae format: + * Writes to hooks.json with { version: 1, hooks: { PreToolUse: [{ matcher, hooks: [{ type, command, timeout }] }] } } + */ +async function installTraeHooks( + baseDir: string, + platformBase: string, + skillsDir: string, + hooksConfig: Record, + platformName: string, + context: HookCommandContext, +): Promise { + const hooksPath = path.join(platformBase, 'hooks.json'); + const matcherGroups: Record< + string, + Array<{ type: string; command: string; timeout: number }> + > = {}; + + for (const [scriptRelPath, config] of Object.entries(hooksConfig)) { + matcherGroups[config.matcher] ??= []; + matcherGroups[config.matcher].push({ + type: 'command', + command: buildHookCommand(baseDir, skillsDir, scriptRelPath, context), + timeout: 30, + }); + } + + const preToolUseEntries = Object.entries(matcherGroups).map(([matcher, hooks]) => ({ + matcher, + hooks, + })); + const hooksFile = await readSettingsJsonObject(hooksPath, platformName); + const existingHooks = (hooksFile.hooks as Record) ?? {}; + const existingPreToolUse = asHookGroup(existingHooks.PreToolUse); + const merged = mergeHookGroups( + existingPreToolUse, + preToolUseEntries, + managedHookScriptPaths(hooksConfig), + ); + + hooksFile.version = hooksFile.version ?? 1; + hooksFile.hooks = { ...existingHooks, PreToolUse: merged }; + await ensureDir(path.dirname(hooksPath)); + await writeFile(hooksPath, JSON.stringify(hooksFile, null, 2) + '\n', 'utf-8'); + return { status: 'installed' }; +} + /** * GitHub Copilot format: * Writes to .github/hooks/comet-guard.json with preToolUse hooks config. diff --git a/domains/skill/uninstall.ts b/domains/skill/uninstall.ts index 3fa8a8535..5fa54fa5a 100644 --- a/domains/skill/uninstall.ts +++ b/domains/skill/uninstall.ts @@ -1047,6 +1047,8 @@ async function removeCometHooksForPlatform( return await removeGeminiHooks(platformBase, scriptRelPaths); case 'windsurf': return await removeWindsurfHooks(platformBase, scriptRelPaths); + case 'trae': + return await removeTraeHooks(platformBase, scriptRelPaths); case 'copilot': return await removeCopilotHooks(platformBase, scriptRelPaths); case 'kiro': @@ -1065,7 +1067,6 @@ async function removeQwenStyleHooks( ): Promise { const settingsPath = path.join(platformBase, 'settings.json'); if (!(await fileExists(settingsPath))) return { removed: 0, failed: 0 }; - let removed = 0; const readResult = await readJsonObjectFile(settingsPath); if (readResult.status === 'missing') return { removed: 0, failed: 0 }; if (readResult.status === 'error') return { removed: 0, failed: 1 }; @@ -1081,6 +1082,7 @@ async function removeQwenStyleHooks( return { removed: 0, failed: 0 }; } + let removed = 0; const filtered = existingPreToolUse.flatMap((group) => { if (!Array.isArray(group.hooks)) return [group]; @@ -1117,7 +1119,6 @@ async function removeGeminiHooks( ): Promise { const settingsPath = path.join(platformBase, 'settings.json'); if (!(await fileExists(settingsPath))) return { removed: 0, failed: 0 }; - let removed = 0; const readResult = await readJsonObjectFile(settingsPath); if (readResult.status === 'missing') return { removed: 0, failed: 0 }; if (readResult.status === 'error') return { removed: 0, failed: 1 }; @@ -1133,7 +1134,30 @@ async function removeGeminiHooks( return { removed: 0, failed: 0 }; } - const filtered = existingBeforeTool.flatMap((group) => { + const result = removeManagedGroupedHooks(existingBeforeTool, scriptRelPaths); + const removed = result.removed; + const filtered = result.groups; + + if (filtered.length === 0) { + delete existingHooks.BeforeTool; + } else { + existingHooks.BeforeTool = filtered; + } + + if (Object.keys(existingHooks).length === 0) { + delete settings.hooks; + } + + await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8'); + return { removed, failed: 0 }; +} + +function removeManagedGroupedHooks( + groups: Array>, + scriptRelPaths: string[], +): { groups: Array>; removed: number } { + let removed = 0; + const filtered = groups.flatMap((group) => { if (!Array.isArray(group.hooks)) return [group]; const hooksBefore = (group.hooks as Array>).length; @@ -1149,18 +1173,7 @@ async function removeGeminiHooks( return [{ ...group, hooks }]; }); - if (filtered.length === 0) { - delete existingHooks.BeforeTool; - } else { - existingHooks.BeforeTool = filtered; - } - - if (Object.keys(existingHooks).length === 0) { - delete settings.hooks; - } - - await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8'); - return { removed, failed: 0 }; + return { groups: filtered, removed }; } async function removeWindsurfHooks( @@ -1169,7 +1182,6 @@ async function removeWindsurfHooks( ): Promise { const hooksPath = path.join(platformBase, 'hooks.json'); if (!(await fileExists(hooksPath))) return { removed: 0, failed: 0 }; - let removed = 0; const readResult = await readJsonObjectFile(hooksPath); if (readResult.status === 'missing') return { removed: 0, failed: 0 }; if (readResult.status === 'error') return { removed: 0, failed: 1 }; @@ -1187,6 +1199,7 @@ async function removeWindsurfHooks( return { removed: 0, failed: 0 }; } + let removed = 0; const filtered = existingPreWrite.filter((entry) => { if (isManagedHookCommand(entry.command, scriptRelPaths)) { removed++; @@ -1209,6 +1222,45 @@ async function removeWindsurfHooks( return { removed, failed: 0 }; } +async function removeTraeHooks( + platformBase: string, + scriptRelPaths: string[], +): Promise { + const hooksPath = path.join(platformBase, 'hooks.json'); + if (!(await fileExists(hooksPath))) return { removed: 0, failed: 0 }; + const readResult = await readJsonObjectFile(hooksPath); + if (readResult.status === 'missing') return { removed: 0, failed: 0 }; + if (readResult.status === 'error') return { removed: 0, failed: 1 }; + const hooksFile = readResult.value; + + const existingHooks = hooksFile.hooks as Record | undefined; + if (!existingHooks) { + return { removed: 0, failed: 0 }; + } + + const existingPreToolUse = existingHooks.PreToolUse as Array> | undefined; + if (!existingPreToolUse || !Array.isArray(existingPreToolUse)) { + return { removed: 0, failed: 0 }; + } + + const result = removeManagedGroupedHooks(existingPreToolUse, scriptRelPaths); + const removed = result.removed; + const filtered = result.groups; + + if (filtered.length === 0) { + delete existingHooks.PreToolUse; + } else { + existingHooks.PreToolUse = filtered; + } + + if (Object.keys(existingHooks).length === 0) { + delete hooksFile.hooks; + } + + await writeFile(hooksPath, JSON.stringify(hooksFile, null, 2) + '\n', 'utf-8'); + return { removed, failed: 0 }; +} + async function removeCopilotHooks( platformBase: string, scriptRelPaths: string[], diff --git a/package-lock.json b/package-lock.json index 54f8e22fa..6d4a671dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@rpamis/comet", - "version": "0.4.0-beta.17", + "version": "0.4.0-beta.18", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@rpamis/comet", - "version": "0.4.0-beta.17", + "version": "0.4.0-beta.18", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index ed7918c61..008b9d955 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rpamis/comet", - "version": "0.4.0-beta.17", + "version": "0.4.0-beta.18", "description": "Agent Skill Harness For Turning Ideas Into Evaluated Workflows", "keywords": [ "comet", diff --git a/platform/install/platforms.ts b/platform/install/platforms.ts index 6fe5216b1..8844d97d4 100644 --- a/platform/install/platforms.ts +++ b/platform/install/platforms.ts @@ -15,6 +15,8 @@ export interface Platform { legacySkillsDirs?: string[]; /** Platform configuration and hook root when it differs from the Skill root. */ configDir?: string; + /** Global platform configuration and hook root when it differs from the global Skill root. */ + globalConfigDir?: string; detectionPaths?: string[]; openspecToolId: string; /** OpenSpec's generated tool root when it differs from Comet's canonical Skill root. */ @@ -36,7 +38,8 @@ export interface Platform { | 'qwen' | 'kiro' | 'qoder' - | 'codebuddy'; + | 'codebuddy' + | 'trae'; /** Hook config filename relative to the platform config root when it differs from the format default. */ hookConfigFile?: string; /** Historical hook config filenames checked during migration and uninstall. */ @@ -63,6 +66,9 @@ export function getPlatformSkillsDirs(platform: Platform, scope: InstallScope): } export function getPlatformConfigDir(platform: Platform, scope: InstallScope): string { + if (scope === 'global' && platform.globalConfigDir) { + return platform.globalConfigDir; + } return platform.configDir ?? getPlatformSkillsDir(platform, scope); } @@ -306,17 +312,23 @@ export const PLATFORMS: Platform[] = [ openspecToolId: 'trae', rulesDir: 'rules', rulesFormat: 'md', + supportsHooks: true, + hookFormat: 'trae', }, { id: 'trae-cn', name: 'Trae CN', skillsDir: '.trae-cn', globalSkillsDir: '.trae-cn', + configDir: '.trae', + globalConfigDir: '.trae-cn', // OpenSpec exposes Trae as one tool id; keep Comet's CN-specific install // directories but reuse the supported OpenSpec Trae integration. openspecToolId: 'trae', rulesDir: 'rules', rulesFormat: 'md', + supportsHooks: true, + hookFormat: 'trae', }, { id: 'zcode', diff --git a/test/app/cli-help.test.ts b/test/app/cli-help.test.ts index 90742c908..dcf47a4b6 100644 --- a/test/app/cli-help.test.ts +++ b/test/app/cli-help.test.ts @@ -29,7 +29,7 @@ describe('CLI help text', () => { expect(help.status, help.stderr).toBe(0); expect(help.stdout).toContain(tagline); expect(packageJson.description).toBe(tagline); - expect(packageJson.version).toBe('0.4.0-beta.17'); + expect(packageJson.version).toBe('0.4.0-beta.18'); }); it('marks bundle as the advanced backend and skill Engine runs as advanced', () => { diff --git a/test/app/doctor.test.ts b/test/app/doctor.test.ts index 7934c07d0..6004bc9ff 100644 --- a/test/app/doctor.test.ts +++ b/test/app/doctor.test.ts @@ -245,6 +245,7 @@ describe('doctor command', () => { expect(git('init', '-b', 'master').status).toBe(0); expect(git('config', 'user.email', 'doctor@example.com').status).toBe(0); expect(git('config', 'user.name', 'Doctor Test').status).toBe(0); + expect(git('config', 'commit.gpgsign', 'false').status).toBe(0); await fs.writeFile(path.join(tmpDir, 'README.md'), '# test\n'); expect(git('add', 'README.md').status).toBe(0); expect(git('commit', '-m', 'test').status).toBe(0); @@ -328,6 +329,7 @@ describe('doctor command', () => { expect(git('init', '-b', 'master').status).toBe(0); expect(git('config', 'user.email', 'doctor@example.com').status).toBe(0); expect(git('config', 'user.name', 'Doctor Test').status).toBe(0); + expect(git('config', 'commit.gpgsign', 'false').status).toBe(0); await fs.writeFile(path.join(tmpDir, 'README.md'), '# test\n'); await writeProjectConfig(tmpDir, defaultProjectConfig('docs')); expect(git('add', 'README.md', '.comet/config.yaml').status).toBe(0); @@ -399,6 +401,7 @@ describe('doctor command', () => { expect(git('init', '-b', 'master').status).toBe(0); expect(git('config', 'user.email', 'doctor@example.com').status).toBe(0); expect(git('config', 'user.name', 'Doctor Test').status).toBe(0); + expect(git('config', 'commit.gpgsign', 'false').status).toBe(0); await fs.writeFile(path.join(tmpDir, 'README.md'), '# test\n'); await writeProjectConfig(tmpDir, defaultProjectConfig('docs')); expect(git('add', 'README.md', '.comet/config.yaml').status).toBe(0); diff --git a/test/domains/bundle/bundle-distribute.test.ts b/test/domains/bundle/bundle-distribute.test.ts index eb94a8c51..a4dc67c95 100644 --- a/test/domains/bundle/bundle-distribute.test.ts +++ b/test/domains/bundle/bundle-distribute.test.ts @@ -523,6 +523,39 @@ describe('Bundle distribution', () => { ); }); + it('writes Trae hooks with version and timeout after executable confirmation', async () => { + await makeReady({ name: 'confirmed-trae-hook-bundle', requiresHooks: true }); + const settingsPath = path.join(projectRoot, '.trae', 'hooks.json'); + + const result = await distributeBundle({ + projectRoot, + name: 'confirmed-trae-hook-bundle', + platforms: ['trae'], + scope: 'project', + confirmedExecutables: true, + }); + const settings = JSON.parse(await fs.readFile(settingsPath, 'utf8')) as { + version: number; + hooks: { + PreToolUse: Array<{ + matcher: string; + hooks: Array<{ type: string; command: string; timeout: number }>; + }>; + }; + }; + + expect(result.platforms[0].written).toContain(settingsPath); + expect(settings.version).toBe(1); + expect(settings.hooks.PreToolUse[0].matcher).toBe('Write|Edit'); + expect(settings.hooks.PreToolUse[0].hooks).toEqual([ + expect.objectContaining({ + type: 'command', + command: expect.stringContaining('verify.mjs'), + timeout: 30, + }), + ]); + }); + it('skips existing target files by default and overwrites only when requested', async () => { await makeReady({ name: 'overwrite-bundle' }); const skillPath = path.join(projectRoot, '.claude', 'skills', 'entry', 'SKILL.md'); diff --git a/test/domains/bundle/bundle-platform.test.ts b/test/domains/bundle/bundle-platform.test.ts index 16956a830..c66339e43 100644 --- a/test/domains/bundle/bundle-platform.test.ts +++ b/test/domains/bundle/bundle-platform.test.ts @@ -158,6 +158,24 @@ describe('Bundle platform compiler', () => { ]); }); + it('plans Trae hooks in project hooks.json while scripts remain under .trae', async () => { + const trae = targets.find((target) => target.id === 'trae')!; + + const report = await compileBundleForPlatform(ir(), trae, { + projectRoot, + scope: 'project', + locale: 'zh', + }); + + expect(report.executableDisclosures).toEqual([ + expect.objectContaining({ + id: 'protect-write', + destination: path.join(projectRoot, '.trae', 'hooks.json'), + command: expect.stringContaining('.trae/skills'), + }), + ]); + }); + it('reports every required and optional capability gap without dropping it silently', async () => { const kimi = targets.find((target) => target.id === 'kimicode')!; diff --git a/test/domains/comet-entry/hook-adapter.test.ts b/test/domains/comet-entry/hook-adapter.test.ts index a40214a0b..57dcb02f2 100644 --- a/test/domains/comet-entry/hook-adapter.test.ts +++ b/test/domains/comet-entry/hook-adapter.test.ts @@ -51,6 +51,14 @@ const PLATFORM_FIXTURES = [ id: 'qoder', single: { tool_name: 'Write', tool_input: { file_path: 'src/qoder.ts' } }, }, + { + id: 'trae', + single: { tool_name: 'Write', tool_input: { file_path: 'src/trae.ts' } }, + }, + { + id: 'trae-cn', + single: { tool_name: 'Write', tool_input: { file_path: 'src/trae-cn.ts' } }, + }, ] as const; describe('Comet Hook platform adapter', () => { diff --git a/test/domains/comet-entry/hook-router-runtime.test.ts b/test/domains/comet-entry/hook-router-runtime.test.ts index e3f88dff3..13096ffd3 100644 --- a/test/domains/comet-entry/hook-router-runtime.test.ts +++ b/test/domains/comet-entry/hook-router-runtime.test.ts @@ -25,9 +25,10 @@ describe('packaged Hook Router worktree isolation', () => { let secondary: string; beforeEach(async () => { - primary = await fs.mkdtemp(path.join(os.tmpdir(), 'comet-router-runtime-primary-')); + const tmpRoot = await fs.realpath(os.tmpdir()); + primary = await fs.mkdtemp(path.join(tmpRoot, 'comet-router-runtime-primary-')); secondary = path.join( - os.tmpdir(), + tmpRoot, `comet-router-runtime-secondary-${Date.now()}-${Math.random().toString(36).slice(2)}`, ); const git = (...args: string[]) => @@ -35,6 +36,7 @@ describe('packaged Hook Router worktree isolation', () => { expect(git('init', '-b', 'master').status).toBe(0); expect(git('config', 'user.email', 'router@example.com').status).toBe(0); expect(git('config', 'user.name', 'Router Test').status).toBe(0); + expect(git('config', 'commit.gpgsign', 'false').status).toBe(0); await fs.writeFile(path.join(primary, 'README.md'), '# worktree\n'); expect(git('add', 'README.md').status).toBe(0); expect(git('commit', '-m', 'initial').status).toBe(0); @@ -124,4 +126,46 @@ describe('packaged Hook Router worktree isolation', () => { expect(result.stderr).toContain('raw-patch-shape'); expect(result.stderr).toContain('only allowed in build'); }); + + it.each(['trae', 'trae-cn'] as const)( + 'enforces Native Shape for %s write payloads through the packaged router', + async (platform) => { + await configureChange(primary, `${platform}-shape`, 'shape'); + const payload = JSON.stringify({ + tool_name: 'Write', + tool_input: { file_path: `src/${platform}.ts` }, + }); + + const result = spawnSync( + process.execPath, + [router, '--platform', platform, '--project-root', primary], + { cwd: primary, input: payload, encoding: 'utf8', timeout: 20_000 }, + ); + + expect(result.status, result.stderr).toBe(2); + expect(result.stderr).toContain(`${platform}-shape`); + expect(result.stderr).toContain('only allowed in build'); + }, + ); + + it.each(['trae', 'trae-cn'] as const)( + 'allows %s write payloads through the packaged router during Native Build', + async (platform) => { + await configureChange(primary, `${platform}-build`, 'build'); + const payload = JSON.stringify({ + tool_name: 'Write', + tool_input: { file_path: `src/${platform}.ts` }, + }); + + const result = spawnSync( + process.execPath, + [router, '--platform', platform, '--project-root', primary], + { cwd: primary, input: payload, encoding: 'utf8', timeout: 20_000 }, + ); + + expect(result.status, result.stderr).toBe(0); + expect(result.stdout).toBe(''); + expect(result.stderr).toBe(''); + }, + ); }); diff --git a/test/domains/skill/platform-inspect.test.ts b/test/domains/skill/platform-inspect.test.ts index 5358ecc8d..ba091b619 100644 --- a/test/domains/skill/platform-inspect.test.ts +++ b/test/domains/skill/platform-inspect.test.ts @@ -39,6 +39,9 @@ function hookConfigPath(baseDir: string, platformId: string): string { return path.join(baseDir, '.gemini', 'settings.json'); case 'windsurf': return path.join(baseDir, '.windsurf', 'hooks.json'); + case 'trae': + case 'trae-cn': + return path.join(baseDir, '.trae', 'hooks.json'); case 'github-copilot': return path.join(baseDir, '.github', 'hooks', 'comet-guard.json'); case 'kiro': @@ -131,6 +134,8 @@ describe('platform component inspection', () => { 'codebuddy', 'gemini', 'windsurf', + 'trae', + 'trae-cn', 'github-copilot', 'kiro', ])('recognizes the managed Hook command in the %s format', async (id) => { @@ -160,6 +165,8 @@ describe('platform component inspection', () => { 'codebuddy', 'gemini', 'windsurf', + 'trae', + 'trae-cn', 'github-copilot', 'kiro', ])('removes only the managed %s Router while preserving user configuration', async (id) => { diff --git a/test/domains/skill/skills.test.ts b/test/domains/skill/skills.test.ts index d6ad2f40b..65334d24e 100644 --- a/test/domains/skill/skills.test.ts +++ b/test/domains/skill/skills.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { spawnSync } from 'child_process'; import { promises as fs } from 'fs'; import path from 'path'; import os from 'os'; @@ -39,12 +40,26 @@ import { reconcileCometHooksForPlatform, reconcileProjectCometHooksForPlatform, } from '../../../domains/skill/hook-lifecycle.js'; +import { removeCometHooksForPlatform } from '../../../domains/skill/uninstall.js'; import { PLATFORMS, type Platform } from '../../../platform/install/platforms.js'; import { artifactLanguageToSkillLanguage, resolveArtifactLanguage, } from '../../../domains/skill/languages.js'; import { assertClassicLayoutInitializationSafe } from '../../../domains/comet-classic/classic-layout-initialization.js'; +import { + createNativeChange, + writeNativeChange, +} from '../../../domains/comet-native/native-change.js'; +import { + defaultProjectConfig, + writeProjectConfig, +} from '../../../domains/comet-native/native-config.js'; +import { + ensureNativeDirectories, + nativeProjectPaths, +} from '../../../domains/comet-native/native-paths.js'; +import { selectNativeChange } from '../../../domains/comet-native/native-selection.js'; import { DEFAULT_WORKFLOW_NATIVE_SNAPSHOT_CONFIG } from '../../../domains/workflow-contract/project-config.js'; describe('skills', () => { @@ -819,6 +834,31 @@ describe('skills', () => { scope: 'project' | 'global' = 'project', ) => `node "${normalized(path.join(baseDir, skillsDir, 'skills', ...currentCometScript.split('/')))}" --platform "${platformId}"${scope === 'project' ? ` --project-root "${normalized(baseDir)}"` : ''}`; + const runManagedHookCommand = (command: string, cwd: string) => + spawnSync(command, { + cwd, + input: JSON.stringify({ + tool_name: 'Write', + tool_input: { file_path: 'src/app.ts' }, + }), + shell: true, + encoding: 'utf8', + timeout: 20_000, + }); + const configureNativeBuildChange = async (projectRoot: string): Promise => { + await writeProjectConfig(projectRoot, defaultProjectConfig('.')); + const paths = await nativeProjectPaths(projectRoot, '.'); + await ensureNativeDirectories(paths); + const change = await createNativeChange({ + paths, + name: 'trae-cn-build', + language: 'en', + verificationProtocol: 'legacy-v1', + }); + change.phase = 'build'; + await writeNativeChange(paths, change); + await selectNativeChange(paths, change.name); + }; it('installs only the unified Router Hook for a Native project', async () => { const codex = PLATFORMS.find((candidate) => candidate.id === 'codex')!; @@ -1529,6 +1569,135 @@ describe('skills', () => { }, ); + it('writes Trae project hooks to hooks.json with versioned PreToolUse groups', async () => { + const platform = PLATFORMS.find((candidate) => candidate.id === 'trae')!; + const hooksPath = path.join(tmpDir, '.trae', 'hooks.json'); + const initialHooks = { + version: 1, + userSetting: 'keep', + hooks: { + PostToolUse: [{ matcher: 'Read', hooks: [{ type: 'command', command: 'echo post' }] }], + PreToolUse: [ + { + matcher: 'Write|Edit', + hooks: [ + { type: 'command', command: 'echo user-write-check', timeout: 5 }, + { type: 'command', command: staleCometCommand, timeout: 10 }, + ], + }, + ], + }, + }; + await fs.mkdir(path.dirname(hooksPath), { recursive: true }); + await fs.writeFile(hooksPath, JSON.stringify(initialHooks), 'utf-8'); + + await configureNativeBuildChange(tmpDir); + await copyCometSkillsForPlatform(tmpDir, platform, false, 'skills', 'project'); + await expect(installCometHooksForPlatform(tmpDir, platform, 'project')).resolves.toEqual({ + status: 'installed', + }); + const firstInstall = JSON.parse(await fs.readFile(hooksPath, 'utf-8')); + + expect(firstInstall.version).toBe(1); + expect(firstInstall.userSetting).toBe('keep'); + expect(firstInstall.hooks.PostToolUse).toEqual(initialHooks.hooks.PostToolUse); + expect(firstInstall.hooks.PreToolUse).toEqual([ + { + matcher: 'Write|Edit', + hooks: [{ type: 'command', command: 'echo user-write-check', timeout: 5 }], + }, + { + matcher: 'Write|Edit', + hooks: [ + { + type: 'command', + command: expectedHookCommand('.trae', 'trae'), + timeout: 30, + }, + ], + }, + ]); + const router = runManagedHookCommand( + firstInstall.hooks.PreToolUse[1].hooks[0].command, + tmpDir, + ); + expect(router.status, router.stderr).toBe(0); + + await installCometHooksForPlatform(tmpDir, platform, 'project'); + const secondInstall = JSON.parse(await fs.readFile(hooksPath, 'utf-8')); + expect(secondInstall).toEqual(firstInstall); + }); + + it('writes and removes Trae CN project hooks from .trae and global hooks from .trae-cn', async () => { + const platform = PLATFORMS.find((candidate) => candidate.id === 'trae-cn')!; + const projectHooksPath = path.join(tmpDir, '.trae', 'hooks.json'); + const globalRoot = path.join(tmpDir, 'home'); + const globalHooksPath = path.join(globalRoot, '.trae-cn', 'hooks.json'); + + await configureNativeBuildChange(tmpDir); + await copyCometSkillsForPlatform(tmpDir, platform, false, 'skills', 'project'); + await copyCometSkillsForPlatform(globalRoot, platform, false, 'skills', 'global'); + await expect(installCometHooksForPlatform(tmpDir, platform, 'project')).resolves.toEqual({ + status: 'installed', + }); + await expect(installCometHooksForPlatform(globalRoot, platform, 'global')).resolves.toEqual({ + status: 'installed', + }); + + const projectHooks = JSON.parse(await fs.readFile(projectHooksPath, 'utf-8')); + const globalHooks = JSON.parse(await fs.readFile(globalHooksPath, 'utf-8')); + expect(projectHooks.hooks.PreToolUse[0].hooks[0]).toMatchObject({ + type: 'command', + command: expectedHookCommand('.trae-cn', 'trae-cn'), + timeout: 30, + }); + expect(globalHooks.hooks.PreToolUse[0].hooks[0]).toMatchObject({ + type: 'command', + command: expectedHookCommand('.trae-cn', 'trae-cn', globalRoot, 'global'), + timeout: 30, + }); + const projectRouter = runManagedHookCommand( + projectHooks.hooks.PreToolUse[0].hooks[0].command, + tmpDir, + ); + const globalRouter = runManagedHookCommand( + globalHooks.hooks.PreToolUse[0].hooks[0].command, + tmpDir, + ); + expect(projectRouter.status, projectRouter.stderr).toBe(0); + expect(globalRouter.status, globalRouter.stderr).toBe(0); + + await expect(removeCometHooksForPlatform(tmpDir, platform, 'project')).resolves.toEqual({ + removed: 1, + failed: 0, + }); + await expect(removeCometHooksForPlatform(globalRoot, platform, 'global')).resolves.toEqual({ + removed: 1, + failed: 0, + }); + const cleanedProjectHooks = JSON.parse(await fs.readFile(projectHooksPath, 'utf-8')); + const cleanedGlobalHooks = JSON.parse(await fs.readFile(globalHooksPath, 'utf-8')); + expect(cleanedProjectHooks.hooks).toBeUndefined(); + expect(cleanedGlobalHooks.hooks).toBeUndefined(); + await expect(fs.access(path.join(tmpDir, '.trae-cn', 'hooks.json'))).rejects.toMatchObject({ + code: 'ENOENT', + }); + }); + + it('leaves invalid Trae hooks byte-for-byte unchanged', async () => { + const platform = PLATFORMS.find((candidate) => candidate.id === 'trae')!; + const hooksPath = path.join(tmpDir, '.trae', 'hooks.json'); + const invalidHooks = '{\r\n "hooks": {\r\n'; + await fs.mkdir(path.dirname(hooksPath), { recursive: true }); + await fs.writeFile(hooksPath, invalidHooks, 'utf-8'); + + const result = await installCometHooksForPlatform(tmpDir, platform, 'project'); + + expect(result.status).toBe('failed'); + expect(result.reason).toContain('Invalid Trae settings'); + await expect(fs.readFile(hooksPath, 'utf-8')).resolves.toBe(invalidHooks); + }); + it('does not add a global CodeBuddy Hook or change unrelated user config', async () => { const platform = PLATFORMS.find((candidate) => candidate.id === 'codebuddy')!; const homeDir = path.join(tmpDir, 'home'); diff --git a/test/domains/skill/uninstall.test.ts b/test/domains/skill/uninstall.test.ts index 09688ef4c..0ecadb71b 100644 --- a/test/domains/skill/uninstall.test.ts +++ b/test/domains/skill/uninstall.test.ts @@ -54,6 +54,8 @@ describe('removeCometHooksForPlatform', () => { { id: 'qwen', configPath: ['.qwen', 'settings.json'] }, { id: 'gemini', configPath: ['.gemini', 'settings.json'] }, { id: 'windsurf', configPath: ['.windsurf', 'hooks.json'] }, + { id: 'trae', configPath: ['.trae', 'hooks.json'] }, + { id: 'trae-cn', configPath: ['.trae', 'hooks.json'] }, ])('fails closed when canonical $id Hook JSON is malformed', async ({ id, configPath }) => { const platform = PLATFORMS.find((candidate) => candidate.id === id)!; const settingsPath = path.join(tmpDir, ...configPath); @@ -72,6 +74,8 @@ describe('removeCometHooksForPlatform', () => { { id: 'qwen', configPath: ['.qwen', 'settings.json'] }, { id: 'gemini', configPath: ['.gemini', 'settings.json'] }, { id: 'windsurf', configPath: ['.windsurf', 'hooks.json'] }, + { id: 'trae', configPath: ['.trae', 'hooks.json'] }, + { id: 'trae-cn', configPath: ['.trae', 'hooks.json'] }, ])('fails closed when canonical $id Hook JSON is an array', async ({ id, configPath }) => { const platform = PLATFORMS.find((candidate) => candidate.id === id)!; const settingsPath = path.join(tmpDir, ...configPath); @@ -136,6 +140,16 @@ describe('removeCometHooksForPlatform', () => { accessPath: ['.windsurf', 'hooks.json'], snapshotPath: ['.windsurf', 'hooks.json'], }, + { + id: 'trae', + accessPath: ['.trae', 'hooks.json'], + snapshotPath: ['.trae', 'hooks.json'], + }, + { + id: 'trae-cn', + accessPath: ['.trae', 'hooks.json'], + snapshotPath: ['.trae', 'hooks.json'], + }, { id: 'kiro', accessPath: ['.kiro', 'hooks'], @@ -216,4 +230,38 @@ describe('removeCometHooksForPlatform', () => { }); await expect(fs.readFile(hooksPath, 'utf8')).resolves.toBe(content); }); + + it.each(['trae', 'trae-cn'])( + 'removes managed %s hooks while preserving user handlers and events', + async (id) => { + const platform = PLATFORMS.find((candidate) => candidate.id === id)!; + const hooksPath = path.join(tmpDir, '.trae', 'hooks.json'); + await installCometHooksForPlatform(tmpDir, platform, 'project'); + const hooks = JSON.parse(await fs.readFile(hooksPath, 'utf8')); + hooks.userSetting = 'keep'; + hooks.hooks.PostToolUse = [ + { matcher: 'Read', hooks: [{ type: 'command', command: 'echo post' }] }, + ]; + hooks.hooks.PreToolUse.unshift({ + matcher: 'Write|Edit', + hooks: [{ type: 'command', command: 'echo user-write-check', timeout: 5 }], + }); + await fs.writeFile(hooksPath, `${JSON.stringify(hooks, null, 2)}\n`, 'utf8'); + + await expect(removeCometHooksForPlatform(tmpDir, platform, 'project')).resolves.toEqual({ + removed: 1, + failed: 0, + }); + + const updated = JSON.parse(await fs.readFile(hooksPath, 'utf8')); + expect(updated.userSetting).toBe('keep'); + expect(updated.hooks.PostToolUse).toEqual(hooks.hooks.PostToolUse); + expect(updated.hooks.PreToolUse).toEqual([ + { + matcher: 'Write|Edit', + hooks: [{ type: 'command', command: 'echo user-write-check', timeout: 5 }], + }, + ]); + }, + ); }); diff --git a/test/platform/detect.test.ts b/test/platform/detect.test.ts index c2520171d..8f6523b96 100644 --- a/test/platform/detect.test.ts +++ b/test/platform/detect.test.ts @@ -120,6 +120,10 @@ describe('detect', () => { expect(traeCn?.openspecToolId).toBe('trae'); expect(traeCn?.rulesDir).toBe('rules'); expect(traeCn?.rulesFormat).toBe('md'); + expect(traeCn?.supportsHooks).toBe(true); + expect(traeCn?.hookFormat).toBe('trae'); + expect(traeCn?.configDir).toBe('.trae'); + expect(traeCn?.globalConfigDir).toBe('.trae-cn'); }); }); diff --git a/test/repository/release-metadata.test.ts b/test/repository/release-metadata.test.ts index b7622998d..ed20137a3 100644 --- a/test/repository/release-metadata.test.ts +++ b/test/repository/release-metadata.test.ts @@ -16,7 +16,7 @@ describe('release metadata', () => { readFileSync(path.join(repositoryRoot, 'assets', 'manifest.json'), 'utf8'), ) as { version: string }; - expect(packageJson.version).toBe('0.4.0-beta.17'); + expect(packageJson.version).toBe('0.4.0-beta.18'); expect(packageLock.version).toBe(packageJson.version); expect(packageLock.packages[''].version).toBe(packageJson.version); expect(assetsManifest.version).toBe(packageJson.version);